diff --git a/.golangci.json b/.golangci.json index 85ec2bc8c..c2b46fa50 100644 --- a/.golangci.json +++ b/.golangci.json @@ -36,6 +36,12 @@ "common-false-positives", "legacy", "std-error-handling" + ], + "rules": [ + { + "linters": ["staticcheck"], + "text": "SA1019.*client\\.Apply" + } ] } }, diff --git a/Makefile b/Makefile index c2ffa36a8..ff517cd96 100644 --- a/Makefile +++ b/Makefile @@ -19,8 +19,8 @@ export PORCHDIR=$(abspath $(CURDIR)) # Base image versions export ALPINE_VERSION ?= 3.23.3 -export GOLANG_BOOKWORM_VERSION ?= 1.25.7-bookworm -export GOLANG_ALPINE_VERSION ?= 1.25.7-alpine +export GOLANG_BOOKWORM_VERSION ?= 1.26.0-bookworm +export GOLANG_ALPINE_VERSION ?= 1.26.0-alpine export DEPLOYPORCHCONFIGDIR ?= $(BUILDDIR)/deploy DEPLOYKPTCONFIGDIR=$(BUILDDIR)/kpt_pkgs diff --git a/api/generated/clientset/versioned/fake/clientset_generated.go b/api/generated/clientset/versioned/fake/clientset_generated.go index 3631bb356..adbfd404e 100644 --- a/api/generated/clientset/versioned/fake/clientset_generated.go +++ b/api/generated/clientset/versioned/fake/clientset_generated.go @@ -32,10 +32,6 @@ import ( // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, // without applying any field management, validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. -// -// DEPRECATED: NewClientset replaces this with support for field management, which significantly improves -// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. -// via --with-applyconfig). func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { @@ -49,8 +45,8 @@ func NewSimpleClientset(objects ...runtime.Object) *Clientset { cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { var opts metav1.ListOptions - if watchActcion, ok := action.(testing.WatchActionImpl); ok { - opts = watchActcion.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions } gvr := action.GetResource() ns := action.GetNamespace() @@ -81,6 +77,17 @@ func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } +// IsWatchListSemanticsUnSupported informs the reflector that this client +// doesn't support WatchList semantics. +// +// This is a synthetic method whose sole purpose is to satisfy the optional +// interface check performed by the reflector. +// Returning true signals that WatchList can NOT be used. +// No additional logic is implemented here. +func (c *Clientset) IsWatchListSemanticsUnSupported() bool { + return true +} + var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} diff --git a/api/generated/informers/externalversions/factory.go b/api/generated/informers/externalversions/factory.go index dbb7cbb18..4ce32393e 100644 --- a/api/generated/informers/externalversions/factory.go +++ b/api/generated/informers/externalversions/factory.go @@ -17,6 +17,7 @@ package externalversions import ( + context "context" reflect "reflect" sync "sync" time "time" @@ -27,6 +28,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" + wait "k8s.io/apimachinery/pkg/util/wait" cache "k8s.io/client-go/tools/cache" ) @@ -41,6 +43,7 @@ type sharedInformerFactory struct { defaultResync time.Duration customResync map[reflect.Type]time.Duration transform cache.TransformFunc + informerName *cache.InformerName informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. @@ -87,6 +90,21 @@ func WithTransform(transform cache.TransformFunc) SharedInformerOption { } } +// WithInformerName sets the InformerName for informer identity used in metrics. +// The InformerName must be created via cache.NewInformerName() at startup, +// which validates global uniqueness. Each informer type will register its +// GVR under this name. +func WithInformerName(informerName *cache.InformerName) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.informerName = informerName + return factory + } +} + +func (f *sharedInformerFactory) InformerName() *cache.InformerName { + return f.informerName +} + // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync) @@ -95,6 +113,7 @@ func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Dur // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. // Listers obtained via this SharedInformerFactory will be subject to the same filters // as specified here. +// // Deprecated: Please use NewSharedInformerFactoryWithOptions instead func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) @@ -120,6 +139,10 @@ func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResy } func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.StartWithContext(wait.ContextForChannel(stopCh)) +} + +func (f *sharedInformerFactory) StartWithContext(ctx context.Context) { f.lock.Lock() defer f.lock.Unlock() @@ -129,15 +152,9 @@ func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { for informerType, informer := range f.informers { if !f.startedInformers[informerType] { - f.wg.Add(1) - // We need a new variable in each loop iteration, - // otherwise the goroutine would use the loop variable - // and that keeps changing. - informer := informer - go func() { - defer f.wg.Done() - informer.Run(stopCh) - }() + f.wg.Go(func() { + informer.RunWithContext(ctx) + }) f.startedInformers[informerType] = true } } @@ -150,9 +167,15 @@ func (f *sharedInformerFactory) Shutdown() { // Will return immediately if there is nothing to wait for. f.wg.Wait() + f.informerName.Release() } func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + result := f.WaitForCacheSyncWithContext(wait.ContextForChannel(stopCh)) + return result.Synced +} + +func (f *sharedInformerFactory) WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult { informers := func() map[reflect.Type]cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() @@ -166,10 +189,31 @@ func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[ref return informers }() - res := map[reflect.Type]bool{} + // Wait for informers to sync, without polling. + cacheSyncs := make([]cache.DoneChecker, 0, len(informers)) + for _, informer := range informers { + cacheSyncs = append(cacheSyncs, informer.HasSyncedChecker()) + } + cache.WaitFor(ctx, "" /* no logging */, cacheSyncs...) + + res := cache.SyncResult{ + Synced: make(map[reflect.Type]bool, len(informers)), + } + failed := false for informType, informer := range informers { - res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + hasSynced := informer.HasSynced() + if !hasSynced { + failed = true + } + res.Synced[informType] = hasSynced } + if failed { + // context.Cause is more informative than ctx.Err(). + // This must be non-nil, otherwise WaitFor wouldn't have stopped + // prematurely. + res.Err = context.Cause(ctx) + } + return res } @@ -191,7 +235,9 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal } informer = newFunc(f.client, resyncPeriod) - informer.SetTransform(f.transform) + if f.transform != nil { + informer.SetTransform(f.transform) + } f.informers[informerType] = informer return informer @@ -202,33 +248,52 @@ func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internal // // It is typically used like this: // -// ctx, cancel := context.Background() +// ctx, cancel := context.WithCancel(context.Background()) // defer cancel() // factory := NewSharedInformerFactory(client, resyncPeriod) // defer factory.WaitForStop() // Returns immediately if nothing was started. // genericInformer := factory.ForResource(resource) // typedInformer := factory.SomeAPIGroup().V1().SomeType() -// factory.Start(ctx.Done()) // Start processing these informers. -// synced := factory.WaitForCacheSync(ctx.Done()) -// for v, ok := range synced { -// if !ok { -// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) -// return -// } +// handle, err := typeInformer.Informer().AddEventHandler(...) +// if err != nil { +// return fmt.Errorf("register event handler: %v", err) +// } +// defer typeInformer.Informer().RemoveEventHandler(handle) // Avoids leaking goroutines. +// factory.StartWithContext(ctx) // Start processing these informers. +// synced := factory.WaitForCacheSyncWithContext(ctx) +// if err := synced.AsError(); err != nil { +// return err +// } +// for v := range synced { +// // Only if desired log some information similar to this. +// fmt.Fprintf(os.Stdout, "cache synced: %s", v) +// } +// +// // Also make sure that all of the initial cache events have been delivered. +// if !WaitFor(ctx, "event handler sync", handle.HasSyncedChecker()) { +// // Must have failed because of context. +// return fmt.Errorf("sync event handler: %w", context.Cause(ctx)) // } // // // Creating informers can also be created after Start, but then // // Start must be called again: // anotherGenericInformer := factory.ForResource(resource) -// factory.Start(ctx.Done()) +// factory.StartWithContext(ctx) type SharedInformerFactory interface { internalinterfaces.SharedInformerFactory // Start initializes all requested informers. They are handled in goroutines // which run until the stop channel gets closed. // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + // + // Contextual logging: StartWithContext should be used instead of Start in code which supports contextual logging. Start(stopCh <-chan struct{}) + // StartWithContext initializes all requested informers. They are handled in goroutines + // which run until the context gets canceled. + // Warning: StartWithContext does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + StartWithContext(ctx context.Context) + // Shutdown marks a factory as shutting down. At that point no new // informers can be started anymore and Start will return without // doing anything. @@ -243,8 +308,14 @@ type SharedInformerFactory interface { // WaitForCacheSync blocks until all started informers' caches were synced // or the stop channel gets closed. + // + // Contextual logging: WaitForCacheSync should be used instead of WaitForCacheSync in code which supports contextual logging. It also returns a more useful result. WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + // WaitForCacheSyncWithContext blocks until all started informers' caches were synced + // or the context gets canceled. + WaitForCacheSyncWithContext(ctx context.Context) cache.SyncResult + // ForResource gives generic access to a shared informer of the matching type. ForResource(resource schema.GroupVersionResource) (GenericInformer, error) diff --git a/api/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/api/generated/informers/externalversions/internalinterfaces/factory_interfaces.go index b6ab42d52..4f07a7c1e 100644 --- a/api/generated/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/api/generated/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -32,7 +32,26 @@ type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexI type SharedInformerFactory interface { Start(stopCh <-chan struct{}) InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer + InformerName() *cache.InformerName } // TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) + +// InformerOptions holds the options for creating an informer. +type InformerOptions struct { + // ResyncPeriod is the resync period for this informer. + // If not set, defaults to 0 (no resync). + ResyncPeriod time.Duration + + // Indexers are the indexers for this informer. + Indexers cache.Indexers + + // InformerName is used to uniquely identify this informer for metrics. + // If not set, metrics will not be published for this informer. + // Use cache.NewInformerName() to create an InformerName at startup. + InformerName *cache.InformerName + + // TweakListOptions is an optional function to modify the list options. + TweakListOptions TweakListOptionsFunc +} diff --git a/api/generated/informers/externalversions/porch/v1alpha1/packagerevision.go b/api/generated/informers/externalversions/porch/v1alpha1/packagerevision.go index e68a7e86c..a9c6e2512 100644 --- a/api/generated/informers/externalversions/porch/v1alpha1/packagerevision.go +++ b/api/generated/informers/externalversions/porch/v1alpha1/packagerevision.go @@ -26,6 +26,7 @@ import ( apiporchv1alpha1 "github.com/kptdev/porch/api/porch/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -47,48 +48,61 @@ type packageRevisionInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewPackageRevisionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredPackageRevisionInformer(client, namespace, resyncPeriod, indexers, nil) + return NewPackageRevisionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredPackageRevisionInformer constructs a new informer for PackageRevision type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredPackageRevisionInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + return NewPackageRevisionInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewPackageRevisionInformerWithOptions constructs a new informer for PackageRevision type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPackageRevisionInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "porch.kpt.dev", Version: "v1alpha1", Resource: "packagerevisions"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisions(namespace).List(context.Background(), options) + return client.PorchV1alpha1().PackageRevisions(namespace).List(context.Background(), opts) }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisions(namespace).Watch(context.Background(), options) + return client.PorchV1alpha1().PackageRevisions(namespace).Watch(context.Background(), opts) }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisions(namespace).List(ctx, options) + return client.PorchV1alpha1().PackageRevisions(namespace).List(ctx, opts) }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisions(namespace).Watch(ctx, options) + return client.PorchV1alpha1().PackageRevisions(namespace).Watch(ctx, opts) }, - }, + }, client), &apiporchv1alpha1.PackageRevision{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *packageRevisionInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredPackageRevisionInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewPackageRevisionInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *packageRevisionInformer) Informer() cache.SharedIndexInformer { diff --git a/api/generated/informers/externalversions/porch/v1alpha1/packagerevisionresources.go b/api/generated/informers/externalversions/porch/v1alpha1/packagerevisionresources.go index 82c7115c7..de3a873b1 100644 --- a/api/generated/informers/externalversions/porch/v1alpha1/packagerevisionresources.go +++ b/api/generated/informers/externalversions/porch/v1alpha1/packagerevisionresources.go @@ -26,6 +26,7 @@ import ( apiporchv1alpha1 "github.com/kptdev/porch/api/porch/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -47,48 +48,61 @@ type packageRevisionResourcesInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewPackageRevisionResourcesInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredPackageRevisionResourcesInformer(client, namespace, resyncPeriod, indexers, nil) + return NewPackageRevisionResourcesInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredPackageRevisionResourcesInformer constructs a new informer for PackageRevisionResources type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredPackageRevisionResourcesInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + return NewPackageRevisionResourcesInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewPackageRevisionResourcesInformerWithOptions constructs a new informer for PackageRevisionResources type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPackageRevisionResourcesInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "porch.kpt.dev", Version: "v1alpha1", Resource: "packagerevisionresourcess"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisionResources(namespace).List(context.Background(), options) + return client.PorchV1alpha1().PackageRevisionResources(namespace).List(context.Background(), opts) }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisionResources(namespace).Watch(context.Background(), options) + return client.PorchV1alpha1().PackageRevisionResources(namespace).Watch(context.Background(), opts) }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisionResources(namespace).List(ctx, options) + return client.PorchV1alpha1().PackageRevisionResources(namespace).List(ctx, opts) }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PackageRevisionResources(namespace).Watch(ctx, options) + return client.PorchV1alpha1().PackageRevisionResources(namespace).Watch(ctx, opts) }, - }, + }, client), &apiporchv1alpha1.PackageRevisionResources{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *packageRevisionResourcesInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredPackageRevisionResourcesInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewPackageRevisionResourcesInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *packageRevisionResourcesInformer) Informer() cache.SharedIndexInformer { diff --git a/api/generated/informers/externalversions/porch/v1alpha1/porchpackage.go b/api/generated/informers/externalversions/porch/v1alpha1/porchpackage.go index 69ac93dd1..dd099199b 100644 --- a/api/generated/informers/externalversions/porch/v1alpha1/porchpackage.go +++ b/api/generated/informers/externalversions/porch/v1alpha1/porchpackage.go @@ -26,6 +26,7 @@ import ( apiporchv1alpha1 "github.com/kptdev/porch/api/porch/v1alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" watch "k8s.io/apimachinery/pkg/watch" cache "k8s.io/client-go/tools/cache" ) @@ -47,48 +48,61 @@ type porchPackageInformer struct { // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewPorchPackageInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredPorchPackageInformer(client, namespace, resyncPeriod, indexers, nil) + return NewPorchPackageInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) } // NewFilteredPorchPackageInformer constructs a new informer for PorchPackage type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. func NewFilteredPorchPackageInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { - return cache.NewSharedIndexInformer( - &cache.ListWatch{ - ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + return NewPorchPackageInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewPorchPackageInformerWithOptions constructs a new informer for PorchPackage type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPorchPackageInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + gvr := schema.GroupVersionResource{Group: "porch.kpt.dev", Version: "v1alpha1", Resource: "porchpackages"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PorchPackages(namespace).List(context.Background(), options) + return client.PorchV1alpha1().PorchPackages(namespace).List(context.Background(), opts) }, - WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PorchPackages(namespace).Watch(context.Background(), options) + return client.PorchV1alpha1().PorchPackages(namespace).Watch(context.Background(), opts) }, - ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PorchPackages(namespace).List(ctx, options) + return client.PorchV1alpha1().PorchPackages(namespace).List(ctx, opts) }, - WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { - tweakListOptions(&options) + tweakListOptions(&opts) } - return client.PorchV1alpha1().PorchPackages(namespace).Watch(ctx, options) + return client.PorchV1alpha1().PorchPackages(namespace).Watch(ctx, opts) }, - }, + }, client), &apiporchv1alpha1.PorchPackage{}, - resyncPeriod, - indexers, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, ) } func (f *porchPackageInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredPorchPackageInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewPorchPackageInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) } func (f *porchPackageInformer) Informer() cache.SharedIndexInformer { diff --git a/api/generated/openapi/zz_generated.openapi.go b/api/generated/openapi/zz_generated.openapi.go index 35e6e240e..a040d224b 100644 --- a/api/generated/openapi/zz_generated.openapi.go +++ b/api/generated/openapi/zz_generated.openapi.go @@ -20,7 +20,10 @@ package generated import ( + resource "k8s.io/apimachinery/pkg/api/resource" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + version "k8s.io/apimachinery/pkg/version" common "k8s.io/kube-openapi/pkg/common" spec "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -70,59 +73,61 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/kptdev/porch/api/porch/v1alpha1.TaskResult": schema_porch_api_porch_v1alpha1_TaskResult(ref), "github.com/kptdev/porch/api/porch/v1alpha1.UpstreamPackage": schema_porch_api_porch_v1alpha1_UpstreamPackage(ref), "github.com/kptdev/porch/api/porch/v1alpha2.PackageRevision": schema_porch_api_porch_v1alpha2_PackageRevision(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": schema_pkg_apis_meta_v1_APIResourceList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": schema_pkg_apis_meta_v1_APIVersions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ApplyOptions": schema_pkg_apis_meta_v1_ApplyOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Condition": schema_pkg_apis_meta_v1_Condition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.CreateOptions": schema_pkg_apis_meta_v1_CreateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.DeleteOptions": schema_pkg_apis_meta_v1_DeleteOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration": schema_pkg_apis_meta_v1_Duration(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldSelectorRequirement": schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1": schema_pkg_apis_meta_v1_FieldsV1(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GetOptions": schema_pkg_apis_meta_v1_GetOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupKind": schema_pkg_apis_meta_v1_GroupKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupResource": schema_pkg_apis_meta_v1_GroupResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersion": schema_pkg_apis_meta_v1_GroupVersion(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery": schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionKind": schema_pkg_apis_meta_v1_GroupVersionKind(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionResource": schema_pkg_apis_meta_v1_GroupVersionResource(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.InternalEvent": schema_pkg_apis_meta_v1_InternalEvent(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector": schema_pkg_apis_meta_v1_LabelSelector(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement": schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.List": schema_pkg_apis_meta_v1_List(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta": schema_pkg_apis_meta_v1_ListMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ListOptions": schema_pkg_apis_meta_v1_ListOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry": schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.MicroTime": schema_pkg_apis_meta_v1_MicroTime(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta": schema_pkg_apis_meta_v1_ObjectMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference": schema_pkg_apis_meta_v1_OwnerReference(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata": schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadataList": schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Patch": schema_pkg_apis_meta_v1_Patch(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.PatchOptions": schema_pkg_apis_meta_v1_PatchOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions": schema_pkg_apis_meta_v1_Preconditions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.RootPaths": schema_pkg_apis_meta_v1_RootPaths(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR": schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Status": schema_pkg_apis_meta_v1_Status(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause": schema_pkg_apis_meta_v1_StatusCause(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails": schema_pkg_apis_meta_v1_StatusDetails(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Table": schema_pkg_apis_meta_v1_Table(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition": schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableOptions": schema_pkg_apis_meta_v1_TableOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow": schema_pkg_apis_meta_v1_TableRow(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition": schema_pkg_apis_meta_v1_TableRowCondition(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Time": schema_pkg_apis_meta_v1_Time(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.Timestamp": schema_pkg_apis_meta_v1_Timestamp(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta": schema_pkg_apis_meta_v1_TypeMeta(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.UpdateOptions": schema_pkg_apis_meta_v1_UpdateOptions(ref), - "k8s.io/apimachinery/pkg/apis/meta/v1.WatchEvent": schema_pkg_apis_meta_v1_WatchEvent(ref), - "k8s.io/apimachinery/pkg/runtime.RawExtension": schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), - "k8s.io/apimachinery/pkg/runtime.TypeMeta": schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), - "k8s.io/apimachinery/pkg/runtime.Unknown": schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), - "k8s.io/apimachinery/pkg/version.Info": schema_k8sio_apimachinery_pkg_version_Info(ref), + resource.Quantity{}.OpenAPIModelName(): schema_apimachinery_pkg_api_resource_Quantity(ref), + v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), + v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), + v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), + v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), + v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), + v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), + v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), + v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), + v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), + v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), + v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), + v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), + v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), + v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), + v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), + v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), + v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), + v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), + v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), + v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), + v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), + v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), + v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), + v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), + v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), + v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), + v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), + v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), + v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), + v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + v1.ShardInfo{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ShardInfo(ref), + v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), + v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), + v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), + v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), + v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), + v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), + v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), + v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), + v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), + v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), + v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), + v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), + runtime.RawExtension{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_RawExtension(ref), + runtime.TypeMeta{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_TypeMeta(ref), + runtime.Unknown{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_runtime_Unknown(ref), + version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), } } @@ -150,7 +155,7 @@ func schema_kptdev_porch_api_porch_PackageRevision(ref common.ReferenceCallback) "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -169,7 +174,7 @@ func schema_kptdev_porch_api_porch_PackageRevision(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch.PackageRevisionSpec", "github.com/kptdev/porch/api/porch.PackageRevisionStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch.PackageRevisionSpec", "github.com/kptdev/porch/api/porch.PackageRevisionStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -197,7 +202,7 @@ func schema_kptdev_porch_api_porch_PackageRevisionResources(ref common.Reference "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -216,7 +221,7 @@ func schema_kptdev_porch_api_porch_PackageRevisionResources(ref common.Reference }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch.PackageRevisionResourcesSpec", "github.com/kptdev/porch/api/porch.PackageRevisionResourcesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch.PackageRevisionResourcesSpec", "github.com/kptdev/porch/api/porch.PackageRevisionResourcesStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -244,7 +249,7 @@ func schema_kptdev_porch_api_porch_PorchPackage(ref common.ReferenceCallback) co "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -263,7 +268,7 @@ func schema_kptdev_porch_api_porch_PorchPackage(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch.PackageSpec", "github.com/kptdev/porch/api/porch.PackageStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch.PackageSpec", "github.com/kptdev/porch/api/porch.PackageStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -686,7 +691,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevision(ref common.ReferenceCallbac "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -705,7 +710,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevision(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -733,7 +738,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionList(ref common.ReferenceCal "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "items": { @@ -754,7 +759,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionList(ref common.ReferenceCal }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevision", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevision", v1.ListMeta{}.OpenAPIModelName()}, } } @@ -804,7 +809,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionResources(ref common.Referen "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -823,7 +828,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionResources(ref common.Referen }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResourcesSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResourcesStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResourcesSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResourcesStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -851,7 +856,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionResourcesList(ref common.Ref "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "items": { @@ -872,7 +877,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionResourcesList(ref common.Ref }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResources", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PackageRevisionResources", v1.ListMeta{}.OpenAPIModelName()}, } } @@ -1071,7 +1076,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC "publishTimestamp": { SchemaProps: spec.SchemaProps{ Description: "PublishedAt is the time when the package revision was approved.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + Ref: ref(v1.Time{}.OpenAPIModelName()), }, }, "deployment": { @@ -1105,7 +1110,7 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.Condition", "github.com/kptdev/porch/api/porch/v1alpha1.Locator", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/kptdev/porch/api/porch/v1alpha1.Condition", "github.com/kptdev/porch/api/porch/v1alpha1.Locator", v1.Time{}.OpenAPIModelName()}, } } @@ -1251,7 +1256,7 @@ func schema_porch_api_porch_v1alpha1_PorchPackage(ref common.ReferenceCallback) "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -1270,7 +1275,7 @@ func schema_porch_api_porch_v1alpha1_PorchPackage(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PackageSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PackageSpec", "github.com/kptdev/porch/api/porch/v1alpha1.PackageStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -1298,7 +1303,7 @@ func schema_porch_api_porch_v1alpha1_PorchPackageList(ref common.ReferenceCallba "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "items": { @@ -1319,7 +1324,7 @@ func schema_porch_api_porch_v1alpha1_PorchPackageList(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.PorchPackage", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.PorchPackage", v1.ListMeta{}.OpenAPIModelName()}, } } @@ -1579,7 +1584,7 @@ func schema_porch_api_porch_v1alpha1_ResultList(ref common.ReferenceCallback) co "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "exitCode": { @@ -1608,7 +1613,7 @@ func schema_porch_api_porch_v1alpha1_ResultList(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha1.Result", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha1.Result", v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -1805,7 +1810,7 @@ func schema_porch_api_porch_v1alpha2_PackageRevision(ref common.ReferenceCallbac "metadata": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, "spec": { @@ -1824,7 +1829,55 @@ func schema_porch_api_porch_v1alpha2_PackageRevision(ref common.ReferenceCallbac }, }, Dependencies: []string{ - "github.com/kptdev/porch/api/porch/v1alpha2.PackageRevisionSpec", "github.com/kptdev/porch/api/porch/v1alpha2.PackageRevisionStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/kptdev/porch/api/porch/v1alpha2.PackageRevisionSpec", "github.com/kptdev/porch/api/porch/v1alpha2.PackageRevisionStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_apimachinery_pkg_api_resource_Quantity(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.EmbedOpenAPIDefinitionIntoV2Extension(common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation.", + OneOf: common.GenerateOpenAPIV3OneOfSchema(resource.Quantity{}.OpenAPIV3OneOfTypes()), + Format: resource.Quantity{}.OpenAPISchemaFormat(), + }, + }, + }, common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` ::= \n\n\t(Note that may be empty, from the \"\" case in .)\n\n ::= 0 | 1 | ... | 9 ::= | ::= | . | . | . ::= \"+\" | \"-\" ::= | ::= | | ::= Ki | Mi | Gi | Ti | Pi | Ei\n\n\t(International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)\n\n ::= m | \"\" | k | M | G | T | P | E\n\n\t(Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)\n\n ::= \"e\" | \"E\" ```\n\nNo matter which of the three exponent forms is used, no quantity may represent a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal places. Numbers larger or more precise will be capped or rounded up. (E.g.: 0.1m will rounded up to 1m.) This may be extended in the future if we require larger or smaller quantities.\n\nWhen a Quantity is parsed from a string, it will remember the type of suffix it had, and will use the same type again when it is serialized.\n\nBefore serializing, Quantity will be put in \"canonical form\". This means that Exponent/suffix will be adjusted up or down (with a corresponding increase or decrease in Mantissa) such that:\n\n- No precision is lost - No fractional digits will be emitted - The exponent (or suffix) is as large as possible.\n\nThe sign will be omitted unless the number is negative.\n\nExamples:\n\n- 1.5 will be serialized as \"1500m\" - 1.5Gi will be serialized as \"1536Mi\"\n\nNote that the quantity will NEVER be internally represented by a floating point number. That is the whole point of this exercise.\n\nNon-canonical values will still parse as long as they are well formed, but will be re-emitted in their canonical form. (So always use canonical form, or don't diff.)\n\nThis format is intended to make it difficult to use these numbers without writing some sort of special handling code in the hopes that that will cause implementors to also use a fixed point implementation.", + Type: resource.Quantity{}.OpenAPISchemaType(), + Format: resource.Quantity{}.OpenAPISchemaFormat(), + }, + }, + }) +} + +func schema_apimachinery_pkg_api_resource_int64Amount(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "int64Amount represents a fixed precision numerator and arbitrary scale exponent. It is faster than operations on inf.Dec for values that can be represented as int64.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "value": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "scale": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"value", "scale"}, + }, + }, } } @@ -1870,7 +1923,7 @@ func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenA Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery"), + Ref: ref(v1.GroupVersionForDiscovery{}.OpenAPIModelName()), }, }, }, @@ -1880,7 +1933,7 @@ func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenA SchemaProps: spec.SchemaProps{ Description: "preferredVersion is the version preferred by the API server, which probably is the storage version.", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery"), + Ref: ref(v1.GroupVersionForDiscovery{}.OpenAPIModelName()), }, }, "serverAddressByClientCIDRs": { @@ -1896,7 +1949,7 @@ func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenA Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR"), + Ref: ref(v1.ServerAddressByClientCIDR{}.OpenAPIModelName()), }, }, }, @@ -1907,7 +1960,7 @@ func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenA }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.GroupVersionForDiscovery", "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR"}, + v1.GroupVersionForDiscovery{}.OpenAPIModelName(), v1.ServerAddressByClientCIDR{}.OpenAPIModelName()}, } } @@ -1945,7 +1998,7 @@ func schema_pkg_apis_meta_v1_APIGroupList(ref common.ReferenceCallback) common.O Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup"), + Ref: ref(v1.APIGroup{}.OpenAPIModelName()), }, }, }, @@ -1956,7 +2009,7 @@ func schema_pkg_apis_meta_v1_APIGroupList(ref common.ReferenceCallback) common.O }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup"}, + v1.APIGroup{}.OpenAPIModelName()}, } } @@ -2124,7 +2177,7 @@ func schema_pkg_apis_meta_v1_APIResourceList(ref common.ReferenceCallback) commo Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.APIResource"), + Ref: ref(v1.APIResource{}.OpenAPIModelName()), }, }, }, @@ -2135,7 +2188,7 @@ func schema_pkg_apis_meta_v1_APIResourceList(ref common.ReferenceCallback) commo }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResource"}, + v1.APIResource{}.OpenAPIModelName()}, } } @@ -2193,7 +2246,7 @@ func schema_pkg_apis_meta_v1_APIVersions(ref common.ReferenceCallback) common.Op Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR"), + Ref: ref(v1.ServerAddressByClientCIDR{}.OpenAPIModelName()), }, }, }, @@ -2204,7 +2257,7 @@ func schema_pkg_apis_meta_v1_APIVersions(ref common.ReferenceCallback) common.Op }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ServerAddressByClientCIDR"}, + v1.ServerAddressByClientCIDR{}.OpenAPIModelName()}, } } @@ -2305,7 +2358,7 @@ func schema_pkg_apis_meta_v1_Condition(ref common.ReferenceCallback) common.Open "lastTransitionTime": { SchemaProps: spec.SchemaProps{ Description: "lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + Ref: ref(v1.Time{}.OpenAPIModelName()), }, }, "reason": { @@ -2329,7 +2382,7 @@ func schema_pkg_apis_meta_v1_Condition(ref common.ReferenceCallback) common.Open }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + v1.Time{}.OpenAPIModelName()}, } } @@ -2425,7 +2478,7 @@ func schema_pkg_apis_meta_v1_DeleteOptions(ref common.ReferenceCallback) common. "preconditions": { SchemaProps: spec.SchemaProps{ Description: "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions"), + Ref: ref(v1.Preconditions{}.OpenAPIModelName()), }, }, "orphanDependents": { @@ -2473,7 +2526,7 @@ func schema_pkg_apis_meta_v1_DeleteOptions(ref common.ReferenceCallback) common. }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Preconditions"}, + v1.Preconditions{}.OpenAPIModelName()}, } } @@ -2785,15 +2838,12 @@ func schema_pkg_apis_meta_v1_InternalEvent(ref common.ReferenceCallback) common. "Object": { SchemaProps: spec.SchemaProps{ Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Bookmark: the object (instance of a type being watched) where\n only ResourceVersion field is set. On successful restart of watch from a\n bookmark resourceVersion, client is guaranteed to not get repeat event\n nor miss any events.\n * If Type is Error: *api.Status is recommended; other types may make sense\n depending on context.", - Ref: ref("k8s.io/apimachinery/pkg/runtime.Object"), }, }, }, Required: []string{"Type", "Object"}, }, }, - Dependencies: []string{ - "k8s.io/apimachinery/pkg/runtime.Object"}, } } @@ -2833,7 +2883,7 @@ func schema_pkg_apis_meta_v1_LabelSelector(ref common.ReferenceCallback) common. Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement"), + Ref: ref(v1.LabelSelectorRequirement{}.OpenAPIModelName()), }, }, }, @@ -2848,7 +2898,7 @@ func schema_pkg_apis_meta_v1_LabelSelector(ref common.ReferenceCallback) common. }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelectorRequirement"}, + v1.LabelSelectorRequirement{}.OpenAPIModelName()}, } } @@ -2927,7 +2977,7 @@ func schema_pkg_apis_meta_v1_List(ref common.ReferenceCallback) common.OpenAPIDe SchemaProps: spec.SchemaProps{ Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "items": { @@ -2937,7 +2987,7 @@ func schema_pkg_apis_meta_v1_List(ref common.ReferenceCallback) common.OpenAPIDe Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), }, }, }, @@ -2948,7 +2998,7 @@ func schema_pkg_apis_meta_v1_List(ref common.ReferenceCallback) common.OpenAPIDe }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + v1.ListMeta{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, } } @@ -2987,9 +3037,17 @@ func schema_pkg_apis_meta_v1_ListMeta(ref common.ReferenceCallback) common.OpenA Format: "int64", }, }, + "shardInfo": { + SchemaProps: spec.SchemaProps{ + Description: "shardInfo is set when the list is a filtered subset of the full collection, as selected by a shard selector on the request. It echoes back the selector so clients can verify which shard they received and merge sharded responses. Clients should not cache sharded list responses as a full representation of the collection.\n\nThis is an alpha field and requires enabling the ShardedListAndWatch feature gate.", + Ref: ref(v1.ShardInfo{}.OpenAPIModelName()), + }, + }, }, }, }, + Dependencies: []string{ + v1.ShardInfo{}.OpenAPIModelName()}, } } @@ -3084,6 +3142,13 @@ func schema_pkg_apis_meta_v1_ListOptions(ref common.ReferenceCallback) common.Op Format: "", }, }, + "shardSelector": { + SchemaProps: spec.SchemaProps{ + Description: "shardSelector restricts the list of returned objects using a CEL-based shard selector expression. The format uses the shardRange() function combined with || (logical OR) to specify one or more hash ranges:\n\n shardRange(object.metadata.uid, '0x0', '0x8000000000000000')\n shardRange(object.metadata.uid, '0x0', '0x8000000000000000') || shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')\n\nField paths use CEL-style object-rooted syntax (e.g. \"object.metadata.uid\"), NOT the fieldSelector format (\"metadata.uid\"). Currently supported paths:\n - object.metadata.uid\n - object.metadata.namespace\n\nhexStart and hexEnd are single-quoted CEL string literals with a '0x' prefix, defining the inclusive lower and exclusive upper bounds over the 64-bit FNV-1a hash space. The full range is [0x0, 0x10000000000000000), where the exclusive upper bound equals 2^64.\n\nExamples:\n 2-shard split:\n shard 0: shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')\n shard 1: shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')\n 4-shard split:\n shard 0: shardRange(object.metadata.uid, '0x0000000000000000', '0x4000000000000000')\n shard 1: shardRange(object.metadata.uid, '0x4000000000000000', '0x8000000000000000')\n shard 2: shardRange(object.metadata.uid, '0x8000000000000000', '0xc000000000000000')\n shard 3: shardRange(object.metadata.uid, '0xc000000000000000', '0x10000000000000000')\n\nThis is an alpha field and requires enabling the ShardedListAndWatch feature gate.", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -3121,7 +3186,7 @@ func schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref common.ReferenceCallback) co "time": { SchemaProps: spec.SchemaProps{ Description: "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + Ref: ref(v1.Time{}.OpenAPIModelName()), }, }, "fieldsType": { @@ -3134,7 +3199,7 @@ func schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref common.ReferenceCallback) co "fieldsV1": { SchemaProps: spec.SchemaProps{ Description: "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1"), + Ref: ref(v1.FieldsV1{}.OpenAPIModelName()), }, }, "subresource": { @@ -3148,7 +3213,7 @@ func schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.FieldsV1", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + v1.FieldsV1{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, } } @@ -3223,13 +3288,13 @@ func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.Ope "creationTimestamp": { SchemaProps: spec.SchemaProps{ Description: "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + Ref: ref(v1.Time{}.OpenAPIModelName()), }, }, "deletionTimestamp": { SchemaProps: spec.SchemaProps{ Description: "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + Ref: ref(v1.Time{}.OpenAPIModelName()), }, }, "deletionGracePeriodSeconds": { @@ -3289,7 +3354,7 @@ func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.Ope Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference"), + Ref: ref(v1.OwnerReference{}.OpenAPIModelName()), }, }, }, @@ -3329,7 +3394,7 @@ func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.Ope Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry"), + Ref: ref(v1.ManagedFieldsEntry{}.OpenAPIModelName()), }, }, }, @@ -3339,7 +3404,7 @@ func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.Ope }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ManagedFieldsEntry", "k8s.io/apimachinery/pkg/apis/meta/v1.OwnerReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + v1.ManagedFieldsEntry{}.OpenAPIModelName(), v1.OwnerReference{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, } } @@ -3433,14 +3498,14 @@ func schema_pkg_apis_meta_v1_PartialObjectMetadata(ref common.ReferenceCallback) SchemaProps: spec.SchemaProps{ Description: "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), }, }, }, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + v1.ObjectMeta{}.OpenAPIModelName()}, } } @@ -3469,7 +3534,7 @@ func schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref common.ReferenceCallb SchemaProps: spec.SchemaProps{ Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "items": { @@ -3480,7 +3545,7 @@ func schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref common.ReferenceCallb Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata"), + Ref: ref(v1.PartialObjectMetadata{}.OpenAPIModelName()), }, }, }, @@ -3491,7 +3556,7 @@ func schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref common.ReferenceCallb }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta", "k8s.io/apimachinery/pkg/apis/meta/v1.PartialObjectMetadata"}, + v1.ListMeta{}.OpenAPIModelName(), v1.PartialObjectMetadata{}.OpenAPIModelName()}, } } @@ -3665,6 +3730,28 @@ func schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref common.ReferenceCallb } } +func schema_pkg_apis_meta_v1_ShardInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ShardInfo describes the shard selector that was applied to produce a list response. Its presence on a list response indicates the list is a filtered subset.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "selector": { + SchemaProps: spec.SchemaProps{ + Description: "selector is the shard selector string from the request, echoed back so clients can verify which shard they received and merge responses from multiple shards.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"selector"}, + }, + }, + } +} + func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -3690,7 +3777,7 @@ func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPI SchemaProps: spec.SchemaProps{ Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "status": { @@ -3715,14 +3802,9 @@ func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPI }, }, "details": { - VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", - }, - }, SchemaProps: spec.SchemaProps{ Description: "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails"), + Ref: ref(v1.StatusDetails{}.OpenAPIModelName()), }, }, "code": { @@ -3736,7 +3818,7 @@ func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPI }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta", "k8s.io/apimachinery/pkg/apis/meta/v1.StatusDetails"}, + v1.ListMeta{}.OpenAPIModelName(), v1.StatusDetails{}.OpenAPIModelName()}, } } @@ -3822,7 +3904,7 @@ func schema_pkg_apis_meta_v1_StatusDetails(ref common.ReferenceCallback) common. Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause"), + Ref: ref(v1.StatusCause{}.OpenAPIModelName()), }, }, }, @@ -3839,7 +3921,7 @@ func schema_pkg_apis_meta_v1_StatusDetails(ref common.ReferenceCallback) common. }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.StatusCause"}, + v1.StatusCause{}.OpenAPIModelName()}, } } @@ -3868,7 +3950,7 @@ func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPID SchemaProps: spec.SchemaProps{ Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), }, }, "columnDefinitions": { @@ -3884,7 +3966,7 @@ func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPID Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition"), + Ref: ref(v1.TableColumnDefinition{}.OpenAPIModelName()), }, }, }, @@ -3903,7 +3985,7 @@ func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPID Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.TableRow"), + Ref: ref(v1.TableRow{}.OpenAPIModelName()), }, }, }, @@ -3914,7 +3996,7 @@ func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPID }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta", "k8s.io/apimachinery/pkg/apis/meta/v1.TableColumnDefinition", "k8s.io/apimachinery/pkg/apis/meta/v1.TableRow"}, + v1.ListMeta{}.OpenAPIModelName(), v1.TableColumnDefinition{}.OpenAPIModelName(), v1.TableRow{}.OpenAPIModelName()}, } } @@ -4045,7 +4127,7 @@ func schema_pkg_apis_meta_v1_TableRow(ref common.ReferenceCallback) common.OpenA Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition"), + Ref: ref(v1.TableRowCondition{}.OpenAPIModelName()), }, }, }, @@ -4054,7 +4136,7 @@ func schema_pkg_apis_meta_v1_TableRow(ref common.ReferenceCallback) common.OpenA "object": { SchemaProps: spec.SchemaProps{ Description: "This field contains the requested additional information about each object based on the includeObject policy when requesting the Table. If \"None\", this field is empty, if \"Object\" this will be the default serialization of the object for the current API version, and if \"Metadata\" (the default) will contain the object metadata. Check the returned kind and apiVersion of the object before parsing. The media type of the object will always match the enclosing list - if this as a JSON table, these will be JSON encoded objects.", - Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), }, }, }, @@ -4062,7 +4144,7 @@ func schema_pkg_apis_meta_v1_TableRow(ref common.ReferenceCallback) common.OpenA }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.TableRowCondition", "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + v1.TableRowCondition{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()}, } } @@ -4257,7 +4339,7 @@ func schema_pkg_apis_meta_v1_WatchEvent(ref common.ReferenceCallback) common.Ope "object": { SchemaProps: spec.SchemaProps{ Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", - Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + Ref: ref(runtime.RawExtension{}.OpenAPIModelName()), }, }, }, @@ -4265,7 +4347,7 @@ func schema_pkg_apis_meta_v1_WatchEvent(ref common.ReferenceCallback) common.Ope }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + runtime.RawExtension{}.OpenAPIModelName()}, } } diff --git a/api/porch/v1alpha2/zz_generated.defaults.go b/api/porch/v1alpha2/zz_generated.defaults.go deleted file mode 100644 index 7286237a0..000000000 --- a/api/porch/v1alpha2/zz_generated.defaults.go +++ /dev/null @@ -1,31 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Copyright 2026 The kpt 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. - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v1alpha2 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/controllers/packagevariants/api/v1alpha1/groupversion_info.go b/controllers/packagevariants/api/v1alpha1/groupversion_info.go index e65b76abf..f8d3bbc09 100644 --- a/controllers/packagevariants/api/v1alpha1/groupversion_info.go +++ b/controllers/packagevariants/api/v1alpha1/groupversion_info.go @@ -18,8 +18,9 @@ package v1alpha1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" ) //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../scripts/boilerplate.go.txt",year=$YEAR_GEN paths="./..." @@ -29,8 +30,17 @@ var ( GroupVersion = schema.GroupVersion{Group: "config.porch.kpt.dev", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &PackageVariant{}, + &PackageVariantList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/controllers/packagevariants/api/v1alpha1/packagevariant_types.go b/controllers/packagevariants/api/v1alpha1/packagevariant_types.go index cd32db94e..27d7279cf 100644 --- a/controllers/packagevariants/api/v1alpha1/packagevariant_types.go +++ b/controllers/packagevariants/api/v1alpha1/packagevariant_types.go @@ -121,7 +121,3 @@ type PackageVariantList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []PackageVariant `json:"items"` } - -func init() { - SchemeBuilder.Register(&PackageVariant{}, &PackageVariantList{}) -} diff --git a/controllers/packagevariants/api/v1alpha1/zz_generated.deepcopy.go b/controllers/packagevariants/api/v1alpha1/zz_generated.deepcopy.go index 41f71556a..ff815f8c3 100644 --- a/controllers/packagevariants/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/packagevariants/api/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha1 import ( "github.com/kptdev/kpt/pkg/api/kptfile/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/controllers/packagevariantsets/api/v1alpha1/groupversion_info.go b/controllers/packagevariantsets/api/v1alpha1/groupversion_info.go index e65b76abf..58ee1474f 100644 --- a/controllers/packagevariantsets/api/v1alpha1/groupversion_info.go +++ b/controllers/packagevariantsets/api/v1alpha1/groupversion_info.go @@ -18,8 +18,9 @@ package v1alpha1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" ) //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../scripts/boilerplate.go.txt",year=$YEAR_GEN paths="./..." @@ -29,8 +30,17 @@ var ( GroupVersion = schema.GroupVersion{Group: "config.porch.kpt.dev", Version: "v1alpha1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &PackageVariantSet{}, + &PackageVariantSetList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/controllers/packagevariantsets/api/v1alpha1/packagevariantset_types.go b/controllers/packagevariantsets/api/v1alpha1/packagevariantset_types.go index 9adcc748e..6ae991901 100644 --- a/controllers/packagevariantsets/api/v1alpha1/packagevariantset_types.go +++ b/controllers/packagevariantsets/api/v1alpha1/packagevariantset_types.go @@ -145,7 +145,3 @@ type PackageVariantSetList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []PackageVariantSet `json:"items"` } - -func init() { - SchemeBuilder.Register(&PackageVariantSet{}, &PackageVariantSetList{}) -} diff --git a/controllers/packagevariantsets/api/v1alpha1/zz_generated.deepcopy.go b/controllers/packagevariantsets/api/v1alpha1/zz_generated.deepcopy.go index dca69860d..e71af3205 100644 --- a/controllers/packagevariantsets/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/packagevariantsets/api/v1alpha1/zz_generated.deepcopy.go @@ -20,7 +20,7 @@ package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go b/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go index 66c6d22db..14a054975 100644 --- a/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go +++ b/controllers/packagevariantsets/api/v1alpha2/groupversion_info.go @@ -18,8 +18,9 @@ package v1alpha2 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" ) //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.19.0 object:headerFile="../../../../scripts/boilerplate.go.txt",year=$YEAR_GEN paths="./..." @@ -29,8 +30,18 @@ var ( GroupVersion = schema.GroupVersion{Group: "config.porch.kpt.dev", Version: "v1alpha2"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &PackageVariantSet{}, + &PackageVariantSetList{}, + ) + + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go b/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go index 457bf7405..686cea6ec 100644 --- a/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go +++ b/controllers/packagevariantsets/api/v1alpha2/packagevariantset_types.go @@ -221,7 +221,3 @@ type PackageVariantSetList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []PackageVariantSet `json:"items"` } - -func init() { - SchemeBuilder.Register(&PackageVariantSet{}, &PackageVariantSetList{}) -} diff --git a/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go b/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go index 3369c7b12..46f4603e6 100644 --- a/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go +++ b/controllers/packagevariantsets/api/v1alpha2/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha2 import ( "github.com/kptdev/porch/controllers/packagevariants/api/v1alpha1" "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/deployments/local/kind_porch_test_cluster.yaml b/deployments/local/kind_porch_test_cluster.yaml index fa444e0f8..62f517c1b 100644 --- a/deployments/local/kind_porch_test_cluster.yaml +++ b/deployments/local/kind_porch_test_cluster.yaml @@ -8,11 +8,11 @@ networking: serviceSubnet: 10.197.0.0/16 nodes: - role: control-plane - image: kindest/node:v1.34.0@sha256:7416a61b42b1662ca6ca89f02028ac133a309a2a30ba309614e8ec94d976dc5a + image: kindest/node:v1.35.1@sha256:05d7bcdefbda08b4e038f644c4df690cdac3fba8b06f8289f30e10026720a1ab kubeadmConfigPatches: - | kind: ClusterConfiguration - apiVersion: kubeadm.k8s.io/v1beta3 + apiVersion: kubeadm.k8s.io/v1beta4 apiServer: extraArgs: request-timeout: "300s" diff --git a/deployments/porch/3-porch-server.yaml b/deployments/porch/3-porch-server.yaml index bbc0840bb..5afffe094 100644 --- a/deployments/porch/3-porch-server.yaml +++ b/deployments/porch/3-porch-server.yaml @@ -94,6 +94,7 @@ spec: - --repo-operation-retry-attempts=3 - --max-request-body-size=6291456 # Keep this in sync with function-runner's corresponding argument - --cache-type=db + - --disable-admission-plugins=MutatingAdmissionPolicy # This can be enabled once kindest/node 1.36.1 is released startupProbe: httpGet: path: /healthz diff --git a/go.mod b/go.mod index bcca49e43..b05773ad3 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/kptdev/porch -go 1.25.7 +go 1.26.0 -replace k8s.io/apiserver v0.34.1 => ./third_party/k8s.io/apiserver-v0.34.1 +replace k8s.io/apiserver v0.36.1 => ./third_party/k8s.io/apiserver-v0.36.1 require ( cloud.google.com/go/iam v1.5.3 @@ -22,8 +22,8 @@ require ( github.com/kptdev/krm-functions-catalog/functions/go/set-namespace v0.4.5 github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5 github.com/kptdev/krm-functions-sdk/go/fn v1.0.2 - github.com/onsi/ginkgo/v2 v2.23.3 - github.com/onsi/gomega v1.37.0 + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/robfig/cron/v3 v3.0.1 @@ -32,8 +32,8 @@ require ( github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0 @@ -45,24 +45,25 @@ require ( google.golang.org/api v0.254.0 google.golang.org/genproto v0.0.0-20251103181224-f26f9409b101 google.golang.org/grpc v1.80.0 - google.golang.org/protobuf v1.36.11 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.34.1 - k8s.io/apiextensions-apiserver v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/apiserver v0.34.1 - k8s.io/cli-runtime v0.34.1 - k8s.io/client-go v0.34.1 - k8s.io/component-base v0.34.1 - k8s.io/klog/v2 v2.130.1 - k8s.io/kube-aggregator v0.34.1 - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 - k8s.io/kubectl v0.34.1 - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + k8s.io/api v0.36.1 + k8s.io/apiextensions-apiserver v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/apiserver v0.36.1 + k8s.io/cli-runtime v0.36.1 + k8s.io/client-go v0.36.1 + k8s.io/component-base v0.36.1 + k8s.io/klog/v2 v2.140.0 + k8s.io/kube-aggregator v0.36.1 + k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af + k8s.io/kubectl v0.36.1 + k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 sigs.k8s.io/cli-utils v0.37.2 - sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/kustomize/kyaml v0.20.1 + sigs.k8s.io/controller-runtime v0.24.1 + sigs.k8s.io/kustomize/kyaml v0.21.1 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 ) @@ -77,7 +78,7 @@ require ( github.com/otiai10/copy v1.14.1 // indirect github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec // indirect go.uber.org/multierr v1.11.0 // indirect - sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect ) require ( @@ -135,7 +136,7 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.18.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v29.4.0+incompatible // indirect @@ -153,30 +154,29 @@ require ( github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect - github.com/go-openapi/swag v0.25.1 // indirect - github.com/go-openapi/swag/cmdutils v0.25.1 // indirect - github.com/go-openapi/swag/conv v0.25.1 // indirect - github.com/go-openapi/swag/fileutils v0.25.1 // indirect - github.com/go-openapi/swag/jsonname v0.25.1 // indirect - github.com/go-openapi/swag/jsonutils v0.25.1 // indirect - github.com/go-openapi/swag/loading v0.25.1 // indirect - github.com/go-openapi/swag/mangling v0.25.1 // indirect - github.com/go-openapi/swag/netutils v0.25.1 // indirect - github.com/go-openapi/swag/stringutils v0.25.1 // indirect - github.com/go-openapi/swag/typeutils v0.25.1 // indirect - github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -192,13 +192,11 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/moby/spdystream v0.5.1 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/otiai10/mint v1.6.3 // indirect @@ -219,14 +217,14 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.etcd.io/etcd/api/v3 v3.6.5 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect - go.etcd.io/etcd/client/v3 v3.6.5 // indirect + go.etcd.io/etcd/api/v3 v3.6.8 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect + go.etcd.io/etcd/client/v3 v3.6.8 // indirect go.mongodb.org/mongo-driver v1.17.6 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect @@ -244,9 +242,9 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gotest.tools/v3 v3.2.0 // indirect - k8s.io/kms v0.34.1 // indirect + k8s.io/kms v0.36.1 // indirect + k8s.io/streaming v0.36.1 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/go.sum b/go.sum index 7650f65f6..16cc17e2c 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.18.0 h1:Ny5yptQgEXSkDFKvlKJG github.com/containerd/stargz-snapshotter/estargz v0.18.0/go.mod h1:7hfU1BO2KB3axZl0dRQCdnHrIWw7TRDdK6L44Rdeuo0= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -116,6 +116,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= @@ -139,34 +145,40 @@ github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= -github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= -github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= -github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= -github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= -github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= -github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= -github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= -github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= -github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= -github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= -github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= -github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= -github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= -github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= -github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= -github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= -github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -210,8 +222,8 @@ github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -224,14 +236,10 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -248,6 +256,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= @@ -285,12 +295,14 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= -github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -306,15 +318,13 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -398,7 +408,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= @@ -422,18 +440,18 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= -go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= -go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= -go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= -go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= -go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= -go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= -go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= -go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA= -go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE= -go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= -go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM= +go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q= +go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50= +go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw= +go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY= +go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8= +go.etcd.io/etcd/pkg/v3 v3.6.8 h1:Xe+LIL974spy8b4nEx3H0KMr1ofq3r0kh6FbU3aw4es= +go.etcd.io/etcd/pkg/v3 v3.6.8/go.mod h1:TRibVNe+FqJIe1abOAA1PsuQ4wqO87ZaOoprg09Tn8c= +go.etcd.io/etcd/server/v3 v3.6.8 h1:U2strdSEy1U8qcSzRIdkYpvOPtBy/9i/IfaaCI9flZ4= +go.etcd.io/etcd/server/v3 v3.6.8/go.mod h1:88dCtwUnSirkUoJbflQxxWXqtBSZa6lSG0Kuej+dois= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= @@ -445,10 +463,10 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rP go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E= go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY= go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw= go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg= go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg= @@ -506,8 +524,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -677,8 +695,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -706,45 +724,47 @@ gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= -k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M= -k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w= -k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-aggregator v0.34.1 h1:WNLV0dVNoFKmuyvdWLd92iDSyD/TSTjqwaPj0U9XAEU= -k8s.io/kube-aggregator v0.34.1/go.mod h1:RU8j+5ERfp0h+gIvWtxRPfsa5nK7rboDm8RST8BJfYQ= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI= -k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks= +k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/cli-runtime v0.36.1 h1:yuC/BGnnj1YYPh6D1P+pZnzinCs6DvMq86yAeNqoqzM= +k8s.io/cli-runtime v0.36.1/go.mod h1:ZQWHGt8xAF7KnviB79vX0lYNyUUqKIpU+LQg7exuFAw= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= +k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kms v0.36.1 h1:XdvKpywoW4k7YUHDh5uYP4mahJXECswHGfCddBBYLZs= +k8s.io/kms v0.36.1/go.mod h1:g91diTD9h0oJCCHkTb00krlF+Qm5HTnkWLi9Q/TpRoc= +k8s.io/kube-aggregator v0.36.1 h1:IzNeRsJcTtgsiCyTgCR1pSwWCrXC1QZQWMTcBw18cFQ= +k8s.io/kube-aggregator v0.36.1/go.mod h1:ROrIm5irUhVUJsKVCgBAAcXpK5IiqpdCn0Ka7LYMGs4= +k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af h1:zLXA2Irn14q2/06WMkxViyr7YCPUO2lJ0QYE9Juy5vA= +k8s.io/kube-openapi v0.0.0-20260520065146-aa012df4f4af/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY= +k8s.io/kubectl v0.36.1 h1:96HqS9twIdHM0MlJLTwbo14b9kUKPkOzZ4tlRDLv4qI= +k8s.io/kubectl v0.36.1/go.mod h1:/DGPAIewKsFWF9VFgGvkPhao2Ev4SNuE3BioZo8yPbk= +k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4= +k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= +k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cli-utils v0.37.2 h1:GOfKw5RV2HDQZDJlru5KkfLO1tbxqMoyn1IYUxqBpNg= sigs.k8s.io/cli-utils v0.37.2/go.mod h1:V+IZZr4UoGj7gMJXklWBg6t5xbdThFBcpj4MrZuCYco= -sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= -sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/make/testing.mk b/make/testing.mk index 78aff10b8..892c6faf0 100644 --- a/make/testing.mk +++ b/make/testing.mk @@ -27,7 +27,7 @@ unit: test test: ## Run unit tests (go test) ifeq ($(CONTAINER_RUNNABLE), 0) - $(RUN_CONTAINER_COMMAND) -e CONTAINER_RUNNABLE golang:1.25.7-bookworm \ + $(RUN_CONTAINER_COMMAND) -e CONTAINER_RUNNABLE golang:"$(GOLANG_BOOKWORM_VERSION)" \ sh -c "useradd -m -s /bin/sh porch && \ mkdir -p ${TEST_COVERAGE_TMP_DIR} && chown porch:porch ${TEST_COVERAGE_TMP_DIR} && \ su porch -c 'export TEST_COVERAGE_TMP_DIR=${TEST_COVERAGE_TMP_DIR}; \ diff --git a/pkg/cache/crcache/cache_test.go b/pkg/cache/crcache/cache_test.go index da3ff3031..5b9b32bc2 100644 --- a/pkg/cache/crcache/cache_test.go +++ b/pkg/cache/crcache/cache_test.go @@ -1,4 +1,4 @@ -// Copyright 2022, 2024-2025 The kpt Authors +// Copyright 2022, 2024-2026 The kpt Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -73,7 +73,7 @@ func TestLatestPackages(t *testing.T) { } if existing, ok := gotLatest[rev.Spec.PackageName]; ok { - t.Errorf("Multiple latest package revisions for package %q: %q and %q", + t.Errorf("Multiple latest package revisions for package %q: %d and %d", rev.Spec.PackageName, rev.Spec.Revision, existing) } diff --git a/pkg/cache/testutil/fake_client.go b/pkg/cache/testutil/fake_client.go index dc15da6e1..887dd83e3 100644 --- a/pkg/cache/testutil/fake_client.go +++ b/pkg/cache/testutil/fake_client.go @@ -66,6 +66,10 @@ func (w *fakeStatusWriter) Create(ctx context.Context, obj client.Object, subres return nil } +func (w *fakeStatusWriter) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + return nil +} + func (f *FakeClientWithStatusUpdate) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) { return watch.NewEmptyWatch(), nil } diff --git a/pkg/registry/porch/packagecommon.go b/pkg/registry/porch/packagecommon.go index cd8c7810c..327b95029 100644 --- a/pkg/registry/porch/packagecommon.go +++ b/pkg/registry/porch/packagecommon.go @@ -367,7 +367,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string, } if isV1Alpha2Repo(&repositoryObj) { - return nil, false, apierrors.NewGone(fmt.Sprintf("repository %q is managed by v1alpha2; use the v1alpha2 API", repositoryID.Name)) + return nil, false, apierrors.NewResourceExpired(fmt.Sprintf("repository %q is managed by v1alpha2; use the v1alpha2 API", repositoryID.Name)) } var parentPackage repository.PackageRevision diff --git a/pkg/registry/porch/packagerevision.go b/pkg/registry/porch/packagerevision.go index 7101e5035..a655efafe 100644 --- a/pkg/registry/porch/packagerevision.go +++ b/pkg/registry/porch/packagerevision.go @@ -180,7 +180,7 @@ func (r *packageRevisions) Create(ctx context.Context, runtimeObject runtime.Obj } if isV1Alpha2Repo(repositoryObj) { - return nil, apierrors.NewGone(fmt.Sprintf("repository %q is managed by v1alpha2; use the v1alpha2 API", repositoryName)) + return nil, apierrors.NewResourceExpired(fmt.Sprintf("repository %q is managed by v1alpha2; use the v1alpha2 API", repositoryName)) } fieldErrors := r.createStrategy.Validate(ctx, runtimeObject) diff --git a/pkg/registry/porch/packagerevision_test.go b/pkg/registry/porch/packagerevision_test.go index 4d71a1720..a9d47addd 100644 --- a/pkg/registry/porch/packagerevision_test.go +++ b/pkg/registry/porch/packagerevision_test.go @@ -295,7 +295,7 @@ func TestCreate(t *testing.T) { result, err = packagerevisions.Create(ctx, newPkgRev, nil, &metav1.CreateOptions{}) assert.Error(t, err) assert.Nil(t, result) - assert.True(t, apierrors.IsGone(err)) + assert.True(t, apierrors.IsResourceExpired(err)) assert.ErrorContains(t, err, "managed by v1alpha2") } diff --git a/scripts/generate-api.sh b/scripts/generate-api.sh index 7c52452f8..303c33958 100755 --- a/scripts/generate-api.sh +++ b/scripts/generate-api.sh @@ -34,7 +34,7 @@ PORCH_API_GENERATED_DIR=$ROOT/api/generated BOILERPLATE=$HERE/boilerplate.go.txt OPENAPI_REPORT=$ROOT/gen_openapi.report -KUBERNETES_VERSION=0.34.1 +KUBERNETES_VERSION=0.36.1 go get "k8s.io/code-generator@v$KUBERNETES_VERSION" CODE_GENERATOR=$(go list -f '{{.Dir}}' -m "k8s.io/code-generator@v$KUBERNETES_VERSION") . "${CODE_GENERATOR}/kube_codegen.sh" diff --git a/test/mockery/mocks/external/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceWriter.go b/test/mockery/mocks/external/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceWriter.go index 8c1126b32..90e2839ce 100644 --- a/test/mockery/mocks/external/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceWriter.go +++ b/test/mockery/mocks/external/sigs.k8s.io/controller-runtime/pkg/client/mock_SubResourceWriter.go @@ -8,6 +8,7 @@ import ( "context" mock "github.com/stretchr/testify/mock" + "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -38,6 +39,82 @@ func (_m *MockSubResourceWriter) EXPECT() *MockSubResourceWriter_Expecter { return &MockSubResourceWriter_Expecter{mock: &_m.Mock} } +// Apply provides a mock function for the type MockSubResourceWriter +func (_mock *MockSubResourceWriter) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error { + // client.SubResourceApplyOption + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, obj) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Apply") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, runtime.ApplyConfiguration, ...client.SubResourceApplyOption) error); ok { + r0 = returnFunc(ctx, obj, opts...) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockSubResourceWriter_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' +type MockSubResourceWriter_Apply_Call struct { + *mock.Call +} + +// Apply is a helper method to define mock.On call +// - ctx context.Context +// - obj runtime.ApplyConfiguration +// - opts ...client.SubResourceApplyOption +func (_e *MockSubResourceWriter_Expecter) Apply(ctx interface{}, obj interface{}, opts ...interface{}) *MockSubResourceWriter_Apply_Call { + return &MockSubResourceWriter_Apply_Call{Call: _e.mock.On("Apply", + append([]interface{}{ctx, obj}, opts...)...)} +} + +func (_c *MockSubResourceWriter_Apply_Call) Run(run func(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption)) *MockSubResourceWriter_Apply_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 runtime.ApplyConfiguration + if args[1] != nil { + arg1 = args[1].(runtime.ApplyConfiguration) + } + var arg2 []client.SubResourceApplyOption + variadicArgs := make([]client.SubResourceApplyOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(client.SubResourceApplyOption) + } + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockSubResourceWriter_Apply_Call) Return(err error) *MockSubResourceWriter_Apply_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockSubResourceWriter_Apply_Call) RunAndReturn(run func(ctx context.Context, obj runtime.ApplyConfiguration, opts ...client.SubResourceApplyOption) error) *MockSubResourceWriter_Apply_Call { + _c.Call.Return(run) + return _c +} + // Create provides a mock function for the type MockSubResourceWriter func (_mock *MockSubResourceWriter) Create(ctx context.Context, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error { // client.SubResourceCreateOption diff --git a/third_party/k8s.io/apiserver-v0.34.1/.import-restrictions b/third_party/k8s.io/apiserver-v0.34.1/.import-restrictions deleted file mode 100644 index 87723dcfc..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/.import-restrictions +++ /dev/null @@ -1,5 +0,0 @@ -rules: - # prevent import of k8s.io/kubernetes - - selectorRegexp: k8s[.]io/kubernetes - forbiddenPrefixes: - - '' diff --git a/third_party/k8s.io/apiserver-v0.34.1/go.mod b/third_party/k8s.io/apiserver-v0.34.1/go.mod deleted file mode 100644 index ae840cf12..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/go.mod +++ /dev/null @@ -1,126 +0,0 @@ -// This is a generated file. Do not edit directly. - -module k8s.io/apiserver - -go 1.25.7 - -godebug default=go1.24 - -require ( - github.com/blang/semver/v4 v4.0.0 - github.com/coreos/go-oidc v2.3.0+incompatible - github.com/coreos/go-systemd/v22 v22.5.0 - github.com/emicklei/go-restful/v3 v3.12.2 - github.com/fsnotify/fsnotify v1.9.0 - github.com/go-logr/logr v1.4.2 - github.com/gogo/protobuf v1.3.2 - github.com/google/btree v1.1.3 - github.com/google/cel-go v0.26.0 - github.com/google/gnostic-models v0.7.0 - github.com/google/go-cmp v0.7.0 - github.com/google/uuid v1.6.0 - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f - github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.10.0 - go.etcd.io/etcd/api/v3 v3.6.4 - go.etcd.io/etcd/client/pkg/v3 v3.6.4 - go.etcd.io/etcd/client/v3 v3.6.4 - go.etcd.io/etcd/server/v3 v3.6.4 - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 - go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 - go.opentelemetry.io/otel/metric v1.35.0 - go.opentelemetry.io/otel/sdk v1.34.0 - go.opentelemetry.io/otel/trace v1.35.0 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.36.0 - golang.org/x/net v0.38.0 - golang.org/x/sync v0.12.0 - golang.org/x/sys v0.31.0 - golang.org/x/time v0.9.0 - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb - google.golang.org/grpc v1.72.1 - google.golang.org/protobuf v1.36.5 - gopkg.in/evanphx/json-patch.v4 v4.12.0 - gopkg.in/go-jose/go-jose.v2 v2.6.3 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 - k8s.io/api v0.34.1 - k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 - k8s.io/component-base v0.34.1 - k8s.io/klog/v2 v2.130.1 - k8s.io/kms v0.34.1 - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 - sigs.k8s.io/randfill v1.0.0 - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 - sigs.k8s.io/yaml v1.6.0 -) - -require ( - cel.dev/expr v0.24.0 // indirect - github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jonboulle/clockwork v0.5.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/moby/spdystream v0.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/pquerna/cachecontrol v0.1.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect - go.etcd.io/bbolt v1.4.2 // indirect - go.etcd.io/etcd/pkg/v3 v3.6.4 // indirect - go.etcd.io/raft/v3 v3.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/plugin.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/plugin.go deleted file mode 100644 index 03aebdd58..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/plugin.go +++ /dev/null @@ -1,221 +0,0 @@ -/* -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 generic - -import ( - "context" - "errors" - "fmt" - - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime/schema" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/initializer" - "k8s.io/apiserver/pkg/admission/plugin/policy/matching" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" -) - -// H is the Hook type generated by the source and consumed by the dispatcher. -// !TODO: Just pass in a Plugin[H] with accessors to all this information -type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H] -type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H] - -// admissionResources is the list of resources related to CEL-based admission -// features. -var admissionResources = []schema.GroupResource{ - {Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"}, - {Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"}, - {Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"}, - {Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"}, -} - -// AdmissionPolicyManager is an abstract admission plugin with all the -// infrastructure to define Admit or Validate on-top. -type Plugin[H any] struct { - *admission.Handler - - sourceFactory sourceFactory[H] - dispatcherFactory dispatcherFactory[H] - - source Source[H] - dispatcher Dispatcher[H] - matcher *matching.Matcher - - informerFactory informers.SharedInformerFactory - client kubernetes.Interface - restMapper meta.RESTMapper - dynamicClient dynamic.Interface - excludedResources sets.Set[schema.GroupResource] - stopCh <-chan struct{} - authorizer authorizer.Authorizer - enabled bool -} - -var ( - _ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{} - _ initializer.WantsExternalKubeClientSet = &Plugin[any]{} - _ initializer.WantsRESTMapper = &Plugin[any]{} - _ initializer.WantsDynamicClient = &Plugin[any]{} - _ initializer.WantsDrainedNotification = &Plugin[any]{} - _ initializer.WantsAuthorizer = &Plugin[any]{} - _ initializer.WantsExcludedAdmissionResources = &Plugin[any]{} - _ admission.InitializationValidator = &Plugin[any]{} -) - -func NewPlugin[H any]( - handler *admission.Handler, - sourceFactory sourceFactory[H], - dispatcherFactory dispatcherFactory[H], -) *Plugin[H] { - return &Plugin[H]{ - Handler: handler, - sourceFactory: sourceFactory, - dispatcherFactory: dispatcherFactory, - - // always exclude admission/mutating policies and bindings - excludedResources: sets.New(admissionResources...), - } -} - -func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { - c.informerFactory = f -} - -func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) { - c.client = client -} - -func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) { - c.restMapper = mapper -} - -func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) { - c.dynamicClient = client -} - -func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) { - c.stopCh = stopCh -} - -func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) { - c.authorizer = authorizer -} - -func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) { - c.matcher = matcher -} - -func (c *Plugin[H]) SetEnabled(enabled bool) { - c.enabled = enabled -} - -func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) { - c.excludedResources.Insert(excludedResources...) -} - -// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller -func (c *Plugin[H]) ValidateInitialization() error { - // By default enabled is set to false. It is up to types which embed this - // struct to set it to true (if feature gate is enabled, or other conditions) - if !c.enabled { - return nil - } - if c.Handler == nil { - return errors.New("missing handler") - } - if c.informerFactory == nil { - return errors.New("missing informer factory") - } - if c.client == nil { - return errors.New("missing kubernetes client") - } - if c.restMapper == nil { - return errors.New("missing rest mapper") - } - if c.dynamicClient == nil { - return errors.New("missing dynamic client") - } - if c.stopCh == nil { - return errors.New("missing stop channel") - } - if c.authorizer == nil { - return errors.New("missing authorizer") - } - - // Use default matcher - namespaceInformer := c.informerFactory.Core().V1().Namespaces() - c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client) - - if err := c.matcher.ValidateInitialization(); err != nil { - return err - } - - c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper) - c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client) - - pluginContext, pluginContextCancel := context.WithCancel(context.Background()) - go func() { - defer pluginContextCancel() - <-c.stopCh - }() - - go func() { - err := c.source.Run(pluginContext) - if err != nil && !errors.Is(err, context.Canceled) { - utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err)) - } - }() - - err := c.dispatcher.Start(pluginContext) - if err != nil && !errors.Is(err, context.Canceled) { - utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err)) - } - - c.SetReadyFunc(func() bool { - return namespaceInformer.Informer().HasSynced() && c.source.HasSynced() - }) - return nil -} - -func (c *Plugin[H]) Dispatch( - ctx context.Context, - a admission.Attributes, - o admission.ObjectInterfaces, -) (err error) { - if !c.enabled { - return nil - } else if c.shouldIgnoreResource(a) { - return nil - } else if !c.WaitForReady() { - return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) - } - - return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks()) -} - -func (c *Plugin[H]) shouldIgnoreResource(attr admission.Attributes) bool { - gvr := attr.GetResource() - // exclusion decision ignores the version. - gr := gvr.GroupResource() - return c.excludedResources.Has(gr) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.conversion.go deleted file mode 100644 index 66aaecbd8..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.conversion.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -/* -Copyright The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by conversion-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - conversion "k8s.io/apimachinery/pkg/conversion" - runtime "k8s.io/apimachinery/pkg/runtime" - webhookadmission "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission" -) - -func init() { - localSchemeBuilder.Register(RegisterConversions) -} - -// RegisterConversions adds conversion functions to the given scheme. -// Public to allow building arbitrary schemes. -func RegisterConversions(s *runtime.Scheme) error { - if err := s.AddGeneratedConversionFunc((*WebhookAdmission)(nil), (*webhookadmission.WebhookAdmission)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_WebhookAdmission_To_webhookadmission_WebhookAdmission(a.(*WebhookAdmission), b.(*webhookadmission.WebhookAdmission), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*webhookadmission.WebhookAdmission)(nil), (*WebhookAdmission)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_webhookadmission_WebhookAdmission_To_v1alpha1_WebhookAdmission(a.(*webhookadmission.WebhookAdmission), b.(*WebhookAdmission), scope) - }); err != nil { - return err - } - return nil -} - -func autoConvert_v1alpha1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *WebhookAdmission, out *webhookadmission.WebhookAdmission, s conversion.Scope) error { - out.KubeConfigFile = in.KubeConfigFile - return nil -} - -// Convert_v1alpha1_WebhookAdmission_To_webhookadmission_WebhookAdmission is an autogenerated conversion function. -func Convert_v1alpha1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *WebhookAdmission, out *webhookadmission.WebhookAdmission, s conversion.Scope) error { - return autoConvert_v1alpha1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in, out, s) -} - -func autoConvert_webhookadmission_WebhookAdmission_To_v1alpha1_WebhookAdmission(in *webhookadmission.WebhookAdmission, out *WebhookAdmission, s conversion.Scope) error { - out.KubeConfigFile = in.KubeConfigFile - return nil -} - -// Convert_webhookadmission_WebhookAdmission_To_v1alpha1_WebhookAdmission is an autogenerated conversion function. -func Convert_webhookadmission_WebhookAdmission_To_v1alpha1_WebhookAdmission(in *webhookadmission.WebhookAdmission, out *WebhookAdmission, s conversion.Scope) error { - return autoConvert_webhookadmission_WebhookAdmission_To_v1alpha1_WebhookAdmission(in, out, s) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig.go deleted file mode 100644 index 7b845f1d1..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2017 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" - "io" - "path" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission" - "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1" - "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1" -) - -var ( - scheme = runtime.NewScheme() - codecs = serializer.NewCodecFactory(scheme) -) - -func init() { - utilruntime.Must(webhookadmission.AddToScheme(scheme)) - utilruntime.Must(v1.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) -} - -// LoadConfig extract the KubeConfigFile from configFile -func LoadConfig(configFile io.Reader) (string, error) { - var kubeconfigFile string - if configFile != nil { - // we have a config so parse it. - data, err := io.ReadAll(configFile) - if err != nil { - return "", err - } - decoder := codecs.UniversalDecoder() - decodedObj, err := runtime.Decode(decoder, data) - if err != nil { - return "", err - } - config, ok := decodedObj.(*webhookadmission.WebhookAdmission) - if !ok { - return "", fmt.Errorf("unexpected type: %T", decodedObj) - } - - if !path.IsAbs(config.KubeConfigFile) { - return "", field.Invalid(field.NewPath("kubeConfigFile"), config.KubeConfigFile, "must be an absolute file path") - } - - kubeconfigFile = config.KubeConfigFile - } - return kubeconfigFile, nil -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go deleted file mode 100644 index 101e68680..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2019 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 ( - "bytes" - "strings" - "testing" -) - -func TestLoadConfig(t *testing.T) { - testcases := []struct { - name string - input string - expectErr string - expectKubeconfig string - }{ - { - name: "empty", - input: "", - expectErr: `'Kind' is missing in ''`, - }, - { - name: "unknown kind", - input: `{"kind":"Unknown","apiVersion":"v1"}`, - expectErr: `no kind "Unknown" is registered for version "v1"`, - }, - { - name: "valid v1alpha1", - input: ` -kind: WebhookAdmission -apiVersion: apiserver.config.k8s.io/v1alpha1 -kubeConfigFile: /foo -`, - expectKubeconfig: "/foo", - }, - { - name: "valid v1", - input: ` -kind: WebhookAdmissionConfiguration -apiVersion: apiserver.config.k8s.io/v1 -kubeConfigFile: /foo -`, - expectKubeconfig: "/foo", - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - kubeconfig, err := LoadConfig(bytes.NewBufferString(tc.input)) - if len(tc.expectErr) > 0 { - if err == nil { - t.Fatal("expected err, got none") - } - if !strings.Contains(err.Error(), tc.expectErr) { - t.Fatalf("expected err containing %q, got %v", tc.expectErr, err) - } - return - } - if err != nil { - t.Fatal(err) - } - if kubeconfig != tc.expectKubeconfig { - t.Fatalf("expected %q, got %q", tc.expectKubeconfig, kubeconfig) - } - }) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context_test.go deleted file mode 100644 index 9606d395c..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context_test.go +++ /dev/null @@ -1,184 +0,0 @@ -/* -Copyright 2021 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 audit - -import ( - "context" - "fmt" - "sync" - "testing" - - auditinternal "k8s.io/apiserver/pkg/apis/audit" - - "github.com/stretchr/testify/assert" -) - -func TestEnabled(t *testing.T) { - tests := []struct { - name string - ctx *AuditContext - expectEnabled bool - }{{ - name: "nil context", - expectEnabled: false, - }, { - name: "empty context", - ctx: &AuditContext{}, - expectEnabled: true, // An AuditContext should be considered enabled before the level is set - }, { - name: "level None", - ctx: func() *AuditContext { - ctx := &AuditContext{} - if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelNone}, nil); err != nil { - t.Fatal(err) - } - return ctx - }(), - expectEnabled: false, - }, { - name: "level Metadata", - ctx: func() *AuditContext { - ctx := &AuditContext{} - if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil { - t.Fatal(err) - } - return ctx - }(), - expectEnabled: true, - }, { - name: "level RequestResponse", - ctx: func() *AuditContext { - ctx := &AuditContext{} - if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, nil); err != nil { - t.Fatal(err) - } - return ctx - }(), - expectEnabled: true, - }} - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expectEnabled, test.ctx.Enabled()) - }) - } -} - -func TestAddAuditAnnotation(t *testing.T) { - const ( - annotationKeyTemplate = "test-annotation-%d" - annotationValue = "test-annotation-value" - annotationExtraValue = "test-existing-annotation" - numAnnotations = 10 - ) - - expectAnnotations := func(t *testing.T, annotations map[string]string) { - assert.Len(t, annotations, numAnnotations) - } - - ctxWithAnnotation := withAuditContextAndLevel(context.Background(), t, auditinternal.LevelMetadata) - AddAuditAnnotation(ctxWithAnnotation, fmt.Sprintf(annotationKeyTemplate, 0), annotationExtraValue) - - tests := []struct { - description string - ctx context.Context - validator func(t *testing.T, ctx context.Context) - }{{ - description: "no audit", - ctx: context.Background(), - validator: func(_ *testing.T, _ context.Context) {}, - }, { - description: "context initialized, policy not evaluated", - // Audit context is initialized, but the policy has not yet been evaluated (no level). - // Annotations should be retained. - ctx: WithAuditContext(context.Background()), - validator: func(t *testing.T, ctx context.Context) { - ev := AuditContextFrom(ctx).event - expectAnnotations(t, ev.Annotations) - }, - }, { - description: "with metadata level", - ctx: withAuditContextAndLevel(context.Background(), t, auditinternal.LevelMetadata), - validator: func(t *testing.T, ctx context.Context) { - ev := AuditContextFrom(ctx).event - expectAnnotations(t, ev.Annotations) - }, - }, { - description: "with none level", - ctx: withAuditContextAndLevel(context.Background(), t, auditinternal.LevelNone), - validator: func(t *testing.T, ctx context.Context) { - ev := AuditContextFrom(ctx).event - assert.Empty(t, ev.Annotations) - }, - }, { - description: "with overlapping annotations", - ctx: ctxWithAnnotation, - validator: func(t *testing.T, ctx context.Context) { - ev := AuditContextFrom(ctx).event - expectAnnotations(t, ev.Annotations) - // Verify that the pre-existing annotation is not overwritten. - assert.Equal(t, annotationExtraValue, ev.Annotations[fmt.Sprintf(annotationKeyTemplate, 0)]) - }, - }} - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(numAnnotations) - for i := 0; i < numAnnotations; i++ { - go func(i int) { - AddAuditAnnotation(test.ctx, fmt.Sprintf(annotationKeyTemplate, i), annotationValue) - wg.Done() - }(i) - } - wg.Wait() - - test.validator(t, test.ctx) - }) - } -} - -func TestAuditAnnotationsWithAuditLoggingSetup(t *testing.T) { - // No audit context data in the request context - ctx := context.Background() - AddAuditAnnotation(ctx, "nil", "0") - - // initialize audit context, policy not evaluated yet - ctx = WithAuditContext(ctx) - AddAuditAnnotation(ctx, "before-evaluation", "1") - - // policy evaluated, audit logging enabled - if err := AuditContextFrom(ctx).Init(RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil { - t.Fatal(err) - } - AddAuditAnnotation(ctx, "after-evaluation", "2") - - expected := map[string]string{ - "before-evaluation": "1", - "after-evaluation": "2", - } - actual := AuditContextFrom(ctx).event.Annotations - assert.Equal(t, expected, actual) -} - -func withAuditContextAndLevel(ctx context.Context, t *testing.T, l auditinternal.Level) context.Context { - ctx = WithAuditContext(ctx) - if err := AuditContextFrom(ctx).Init(RequestAuditConfig{Level: l}, nil); err != nil { - t.Fatal(err) - } - return ctx -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics.go deleted file mode 100644 index 816cf177f..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2023 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 aggregated - -import ( - "k8s.io/component-base/metrics" - "k8s.io/component-base/metrics/legacyregistry" -) - -var ( - regenerationCounter = metrics.NewCounter( - &metrics.CounterOpts{ - Name: "aggregator_discovery_aggregation_count_total", - Help: "Counter of number of times discovery was aggregated", - StabilityLevel: metrics.ALPHA, - }, - ) -) - -func init() { - legacyregistry.MustRegister(regenerationCounter) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics_test.go deleted file mode 100644 index 0fb69f535..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/metrics_test.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright 2022 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 aggregated_test - -import ( - "fmt" - "io" - "strings" - "testing" - - "k8s.io/component-base/metrics/legacyregistry" - "k8s.io/component-base/metrics/testutil" - - discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" -) - -func formatExpectedMetrics(aggregationCount int) io.Reader { - expected := `` - if aggregationCount > 0 { - expected = expected + `# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated -# TYPE aggregator_discovery_aggregation_count_total counter -aggregator_discovery_aggregation_count_total %d -` - } - args := []any{} - if aggregationCount > 0 { - args = append(args, aggregationCount) - } - return strings.NewReader(fmt.Sprintf(expected, args...)) -} - -func TestBasicMetrics(t *testing.T) { - legacyregistry.Reset() - manager := discoveryendpoint.NewResourceManager("apis") - - apis := fuzzAPIGroups(1, 3, 10) - manager.SetGroups(apis.Items) - - interests := []string{"aggregator_discovery_aggregation_count_total"} - - _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") - // A single fetch should aggregate and increment regeneration counter. - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { - t.Fatal(err) - } - _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") - // Subsequent fetches should not reaggregate discovery. - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { - t.Fatal(err) - } -} - -func TestMetricsModified(t *testing.T) { - legacyregistry.Reset() - manager := discoveryendpoint.NewResourceManager("apis") - - apis := fuzzAPIGroups(1, 3, 10) - manager.SetGroups(apis.Items) - - interests := []string{"aggregator_discovery_aggregation_count_total"} - - _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") - // A single fetch should aggregate and increment regeneration counter. - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { - t.Fatal(err) - } - - // Update discovery document. - manager.SetGroups(fuzzAPIGroups(1, 3, 10).Items) - _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") - // If the discovery content has changed, reaggregation should be performed. - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(2), interests...); err != nil { - t.Fatal(err) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete_test.go deleted file mode 100644 index c0396ee4a..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete_test.go +++ /dev/null @@ -1,530 +0,0 @@ -/* -Copyright 2019 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 handlers - -import ( - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - - "k8s.io/apimachinery/pkg/api/errors" - metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/apiserver/pkg/admission" - auditinternal "k8s.io/apiserver/pkg/apis/audit" - "k8s.io/apiserver/pkg/audit" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/rest" - - "k8s.io/utils/ptr" -) - -type mockCodecs struct { - serializer.CodecFactory - err error -} - -type mockCodec struct { - runtime.Codec - codecs *mockCodecs -} - -func (p mockCodec) Encode(obj runtime.Object, w io.Writer) error { - err := p.Codec.Encode(obj, w) - p.codecs.err = err - return err -} - -func (s *mockCodecs) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { - out := s.CodecFactory.CodecForVersions(encoder, nil, gv, nil) - return &mockCodec{ - Codec: out, - codecs: s, - } -} - -func TestDeleteResourceAuditLogRequestObject(t *testing.T) { - - ctx := audit.WithAuditContext(context.TODO()) - ac := audit.AuditContextFrom(ctx) - if err := ac.Init(audit.RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, nil); err != nil { - t.Fatal(err) - } - - policy := metav1.DeletePropagationBackground - deleteOption := &metav1.DeleteOptions{ - GracePeriodSeconds: ptr.To[int64](30), - PropagationPolicy: &policy, - } - - fakeCorev1GroupVersion := schema.GroupVersion{ - Group: "", - Version: "v1", - } - testScheme := runtime.NewScheme() - metav1.AddToGroupVersion(testScheme, fakeCorev1GroupVersion) - testCodec := serializer.NewCodecFactory(testScheme) - - tests := []struct { - name string - object runtime.Object - gv schema.GroupVersion - serializer serializer.CodecFactory - ok bool - }{ - { - name: "meta built-in Codec encode v1.DeleteOptions", - object: &metav1.DeleteOptions{ - GracePeriodSeconds: ptr.To[int64](30), - PropagationPolicy: &policy, - }, - gv: metav1.SchemeGroupVersion, - serializer: metainternalversionscheme.Codecs, - ok: true, - }, - { - name: "fake corev1 registered codec encode v1 DeleteOptions", - object: &metav1.DeleteOptions{ - GracePeriodSeconds: ptr.To[int64](30), - PropagationPolicy: &policy, - }, - gv: metav1.SchemeGroupVersion, - serializer: testCodec, - ok: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - - codecs := &mockCodecs{} - codecs.CodecFactory = test.serializer - - audit.LogRequestObject(ctx, deleteOption, test.gv, schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "pods", - }, "", codecs) - - err := codecs.err - if err != nil { - if test.ok { - t.Errorf("expect nil but got %#v", err) - } - t.Logf("encode object: %#v", err) - } else { - if !test.ok { - t.Errorf("expect err but got nil") - } - } - }) - } -} - -func TestDeleteCollection(t *testing.T) { - req := &http.Request{ - Header: http.Header{}, - } - req.Header.Set("Content-Type", "application/json") - - fakeCorev1GroupVersion := schema.GroupVersion{ - Group: "", - Version: "v1", - } - fakeCorev1Scheme := runtime.NewScheme() - fakeCorev1Scheme.AddKnownTypes(fakeCorev1GroupVersion, &metav1.DeleteOptions{}) - fakeCorev1Codec := serializer.NewCodecFactory(fakeCorev1Scheme) - - tests := []struct { - name string - codecFactory serializer.CodecFactory - data []byte - expectErr string - }{ - // for issue: https://github.com/kubernetes/kubernetes/issues/111985 - { - name: "decode '{}' to metav1.DeleteOptions with fakeCorev1Codecs", - codecFactory: fakeCorev1Codec, - data: []byte("{}"), - expectErr: "no kind \"DeleteOptions\" is registered", - }, - { - name: "decode '{}' to metav1.DeleteOptions with metainternalversionscheme.Codecs", - codecFactory: metainternalversionscheme.Codecs, - data: []byte("{}"), - expectErr: "", - }, - { - name: "decode versioned (corev1) DeleteOptions with metainternalversionscheme.Codecs", - codecFactory: metainternalversionscheme.Codecs, - data: []byte(`{"apiVersion":"v1","kind":"DeleteOptions","gracePeriodSeconds":123}`), - expectErr: "", - }, - { - name: "decode versioned (foo) DeleteOptions with metainternalversionscheme.Codecs", - codecFactory: metainternalversionscheme.Codecs, - data: []byte(`{"apiVersion":"foo/v1","kind":"DeleteOptions","gracePeriodSeconds":123}`), - expectErr: "", - }, - } - - defaultGVK := metav1.SchemeGroupVersion.WithKind("DeleteOptions") - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - s, err := negotiation.NegotiateInputSerializer(req, false, test.codecFactory) - if err != nil { - t.Fatal(err) - } - - options := &metav1.DeleteOptions{} - _, _, err = metainternalversionscheme.Codecs.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(test.data, &defaultGVK, options) - if test.expectErr != "" { - if err == nil { - t.Fatalf("expect %s but got nil", test.expectErr) - } - if !strings.Contains(err.Error(), test.expectErr) { - t.Fatalf("expect %s but got %s", test.expectErr, err.Error()) - } - } - }) - } -} - -func TestDeleteCollectionWithNoContextDeadlineEnforced(t *testing.T) { - ctx := t.Context() - var invokedGot, hasDeadlineGot int32 - fakeDeleterFn := func(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *metainternalversion.ListOptions) (runtime.Object, error) { - // we expect CollectionDeleter to be executed once - atomic.AddInt32(&invokedGot, 1) - - // we don't expect any context deadline to be set - if _, hasDeadline := ctx.Deadline(); hasDeadline { - atomic.AddInt32(&hasDeadlineGot, 1) - } - return nil, nil - } - - // do the minimum setup to ensure that it gets as far as CollectionDeleter - scope := &RequestScope{ - Namer: &mockNamer{}, - Serializer: &fakeSerializer{ - serializer: runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{}), - }, - } - handler := DeleteCollection(fakeCollectionDeleterFunc(fakeDeleterFn), false, scope, nil) - - request, err := http.NewRequestWithContext(ctx, request.MethodGet, "/test", nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // the request context should not have any deadline by default - if _, hasDeadline := request.Context().Deadline(); hasDeadline { - t.Fatalf("expected request context to not have any deadline") - } - - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - if atomic.LoadInt32(&invokedGot) != 1 { - t.Errorf("expected collection deleter to be invoked") - } - if atomic.LoadInt32(&hasDeadlineGot) > 0 { - t.Errorf("expected context to not have any deadline") - } -} - -type fakeCollectionDeleterFunc func(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) - -func (f fakeCollectionDeleterFunc) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { - return f(ctx, deleteValidation, options, listOptions) -} - -type fakeSerializer struct { - serializer runtime.Serializer -} - -func (n *fakeSerializer) SupportedMediaTypes() []runtime.SerializerInfo { - return []runtime.SerializerInfo{ - { - MediaType: "application/json", - MediaTypeType: "application", - MediaTypeSubType: "json", - }, - } -} -func (n *fakeSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { - return n.serializer -} -func (n *fakeSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { - return n.serializer -} - -func TestAuthorizeUnsafeDelete(t *testing.T) { - const verbWant = "unsafe-delete-ignore-read-errors" - tests := []struct { - name string - reqInfo *request.RequestInfo - attr admission.Attributes - authz authorizer.Authorizer - err func(admission.Attributes) error - }{ - { - name: "operation is not delete, admit", - attr: newAttributes(attributes{operation: admission.Update}), - authz: nil, // Authorize should not be invoked - }, - { - name: "feature enabled, delete, operation option is nil, admit", - attr: newAttributes(attributes{ - operation: admission.Delete, - operationOptions: nil, - }), - authz: nil, // Authorize should not be invoked - }, - { - name: "delete, operation option is not a match, forbid", - attr: newAttributes(attributes{ - operation: admission.Delete, - operationOptions: &metav1.PatchOptions{}, - }), - authz: nil, // Authorize should not be invoked - err: func(admission.Attributes) error { - return errors.NewInternalError(fmt.Errorf("expected an option of type: %T, but got: %T", &metav1.DeleteOptions{}, &metav1.PatchOptions{})) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is nil, admit", - attr: newAttributes(attributes{ - operation: admission.Delete, - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: nil, - }, - }), - authz: nil, // Authorize should not be invoked - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is false, admit", - attr: newAttributes(attributes{ - operation: admission.Delete, - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](false), - }, - }), - authz: nil, // Authorize should not be invoked - }, - { - name: "feature enabled, delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, no RequestInfo in request context, forbid", - reqInfo: nil, - attr: newAttributes(attributes{ - operation: admission.Delete, - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: nil, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("no RequestInfo found in the context")) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, subresource request, forbid", - reqInfo: &request.RequestInfo{IsResourceRequest: true}, - attr: newAttributes(attributes{ - operation: admission.Delete, - subresource: "foo", - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: nil, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("ignoreStoreReadErrorWithClusterBreakingPotential delete option is not allowed on a subresource or non-resource request")) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, subresource request, forbid", - reqInfo: &request.RequestInfo{IsResourceRequest: false}, - attr: newAttributes(attributes{ - operation: admission.Delete, - subresource: "", - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: nil, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("ignoreStoreReadErrorWithClusterBreakingPotential delete option is not allowed on a subresource or non-resource request")) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, authorizer returns error, forbid", - reqInfo: &request.RequestInfo{IsResourceRequest: true}, - attr: newAttributes(attributes{ - subresource: "", - operation: admission.Delete, - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: &fakeAuthorizer{err: fmt.Errorf("unexpected error")}, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("error while checking permission for %q, %w", verbWant, fmt.Errorf("unexpected error"))) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, user does not have permission, forbid", - reqInfo: &request.RequestInfo{IsResourceRequest: true}, - attr: newAttributes(attributes{ - operation: admission.Delete, - subresource: "", - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: &fakeAuthorizer{ - decision: authorizer.DecisionDeny, - reason: "does not have permission", - }, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("not permitted to do %q, reason: %s", verbWant, "does not have permission")) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, authorizer gives no opinion, forbid", - reqInfo: &request.RequestInfo{IsResourceRequest: true}, - attr: newAttributes(attributes{ - operation: admission.Delete, - subresource: "", - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - }), - authz: &fakeAuthorizer{ - decision: authorizer.DecisionNoOpinion, - reason: "no opinion", - }, - err: func(attr admission.Attributes) error { - return admission.NewForbidden(attr, fmt.Errorf("not permitted to do %q, reason: %s", verbWant, "no opinion")) - }, - }, - { - name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, user has permission, admit", - reqInfo: &request.RequestInfo{IsResourceRequest: true}, - attr: newAttributes(attributes{ - operation: admission.Delete, - subresource: "", - operationOptions: &metav1.DeleteOptions{ - IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), - }, - userInfo: &user.DefaultInfo{Name: "foo"}, - }), - authz: &fakeAuthorizer{ - decision: authorizer.DecisionAllow, - reason: "permitted", - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var want error - if test.err != nil { - want = test.err(test.attr) - } - - ctx := context.Background() - if test.reqInfo != nil { - ctx = request.WithRequestInfo(ctx, test.reqInfo) - } - - // wrap the attributes so we can access the annotations set during admission - attrs := &fakeAttributes{Attributes: test.attr} - got := authorizeUnsafeDelete(ctx, attrs, test.authz) - switch { - case want != nil: - if got == nil || want.Error() != got.Error() { - t.Errorf("expected error: %v, but got: %v", want, got) - } - default: - if got != nil { - t.Errorf("expected no error, but got: %v", got) - } - } - }) - } -} - -// attributes of interest for this test -type attributes struct { - operation admission.Operation - operationOptions runtime.Object - userInfo user.Info - subresource string -} - -func newAttributes(attr attributes) admission.Attributes { - return admission.NewAttributesRecord( - nil, // this plugin should never inspect the object - nil, // old object, this plugin should never inspect it - schema.GroupVersionKind{}, // this plugin should never inspect kind - "", // namespace, leave it empty, this plugin only passes it along to the authorizer - "", // name, leave it empty, this plugin only passes it along to the authorizer - schema.GroupVersionResource{}, // resource, leave it empty, this plugin only passes it along to the authorizer - attr.subresource, - attr.operation, - attr.operationOptions, - false, // dryRun, this plugin should never inspect this attribute - attr.userInfo) -} - -type fakeAttributes struct { - admission.Attributes - annotations map[string]string -} - -func (f *fakeAttributes) AddAnnotation(key, value string) error { - if err := f.Attributes.AddAnnotation(key, value); err != nil { - return err - } - - if len(f.annotations) == 0 { - f.annotations = map[string]string{} - } - f.annotations[key] = value - return nil -} - -type fakeAuthorizer struct { - decision authorizer.Decision - reason string - err error -} - -func (authorizer fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - return authorizer.decision, authorizer.reason, authorizer.err -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate.go deleted file mode 100644 index 2235a4760..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate.go +++ /dev/null @@ -1,322 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rest - -import ( - "context" - "fmt" - "strings" - - "k8s.io/apimachinery/pkg/api/operation" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - genericapirequest "k8s.io/apiserver/pkg/endpoints/request" - validationmetrics "k8s.io/apiserver/pkg/validation" - "k8s.io/klog/v2" -) - -// ValidationConfig defines how a declarative validation request may be configured. -type ValidationConfig func(*validationConfigOption) - -// WithOptions sets the validation options. -// Options should contain any validation options that the declarative validation -// tags expect. These often correspond to feature gates. -func WithOptions(options []string) ValidationConfig { - return func(config *validationConfigOption) { - config.options = options - } -} - -// WithTakeover sets the takeover flag for validation. -func WithTakeover(takeover bool) ValidationConfig { - return func(config *validationConfigOption) { - config.takeover = takeover - } -} - -// WithSubresourceMapper sets the subresource mapper for validation. -// This should be used when registering validation for polymorphic subresources like /scale. -// -// For example, the deployments/scale subresource mapper might map from: -// -// group: apps, version: v1, subresource=scale -// -// to a target of: -// -// group: autoscaling, version: v1, kind=Scale -// -// When set, the group version in the requestInfo of the ctx provided to a declarative validation -// request will be passed to the subresource mapper to find the group version kind of the subresource. -// Declarative validation will then convert the object to the subresource group version kind and validate it. -// -// Note that the target of the mapping contains no subresource part since the mapper is expected to -// map to the group version kind of the subresource. -func WithSubresourceMapper(subresourceMapper GroupVersionKindProvider) ValidationConfig { - return func(config *validationConfigOption) { - config.subresourceGVKMapper = subresourceMapper - } -} - -type validationConfigOption struct { - opType operation.Type - options []string - takeover bool - subresourceGVKMapper GroupVersionKindProvider -} - -// ValidateDeclaratively validates obj against declarative validation tags -// defined in its Go type. It uses the API version extracted from ctx and the -// provided scheme for validation. -// -// The ctx MUST contain requestInfo, which determines the target API for -// validation. The obj is converted to the API version using the provided scheme -// before validation occurs. The scheme MUST have the declarative validation -// registered for the requested resource/subresource. -// -// Returns a field.ErrorList containing any validation errors. An internal error -// is included if requestInfo is missing from the context or if version -// conversion fails. -func ValidateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj runtime.Object, configOpts ...ValidationConfig) field.ErrorList { - cfg := &validationConfigOption{opType: operation.Create} - for _, o := range configOpts { - o(cfg) - } - - return panicSafeValidateFunc(validateDeclaratively, cfg.takeover)(ctx, scheme, obj, nil, cfg) -} - -// ValidateUpdateDeclaratively validates obj and oldObj against declarative -// validation tags defined in its Go type. It uses the API version extracted from -// ctx and the provided scheme for validation. -// -// The ctx MUST contain requestInfo, which determines the target API for -// validation. The obj is converted to the API version using the provided scheme -// before validation occurs. The scheme MUST have the declarative validation -// registered for the requested resource/subresource. -// -// Returns a field.ErrorList containing any validation errors. An internal error -// is included if requestInfo is missing from the context or if version -// conversion fails. -func ValidateUpdateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, configOpts ...ValidationConfig) field.ErrorList { - cfg := &validationConfigOption{opType: operation.Update} - for _, o := range configOpts { - o(cfg) - } - return panicSafeValidateFunc(validateDeclaratively, cfg.takeover)(ctx, scheme, obj, oldObj, cfg) -} - -func validateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList { - // Find versionedGroupVersion, which identifies the API version to use for declarative validation. - versionedGroupVersion, subresources, err := requestInfo(ctx, o.subresourceGVKMapper) - if err != nil { - return field.ErrorList{field.InternalError(nil, err)} - } - versionedObj, err := scheme.ConvertToVersion(obj, versionedGroupVersion) - if err != nil { - return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))} - } - var versionedOldObj runtime.Object - - switch o.opType { - case operation.Create: - return scheme.Validate(ctx, o.options, versionedObj, subresources...) - case operation.Update: - versionedOldObj, err = scheme.ConvertToVersion(oldObj, versionedGroupVersion) - if err != nil { - return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))} - } - return scheme.ValidateUpdate(ctx, o.options, versionedObj, versionedOldObj, subresources...) - default: - return field.ErrorList{field.InternalError(nil, fmt.Errorf("unknown operation type: %v", o.opType))} - } -} - -func requestInfo(ctx context.Context, subresourceMapper GroupVersionKindProvider) (schema.GroupVersion, []string, error) { - requestInfo, found := genericapirequest.RequestInfoFrom(ctx) - if !found { - return schema.GroupVersion{}, nil, fmt.Errorf("could not find requestInfo in context") - } - groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} - if subresourceMapper != nil { - groupVersion = subresourceMapper.GroupVersionKind(groupVersion).GroupVersion() - } - subresources, err := parseSubresourcePath(requestInfo.Subresource) - if err != nil { - return schema.GroupVersion{}, nil, fmt.Errorf("unexpected error parsing subresource path: %w", err) - } - return groupVersion, subresources, nil - -} - -func parseSubresourcePath(subresourcePath string) ([]string, error) { - if len(subresourcePath) == 0 { - return nil, nil - } - parts := strings.Split(subresourcePath, "/") - for _, part := range parts { - if len(part) == 0 { - return nil, fmt.Errorf("invalid subresource path: %s", subresourcePath) - } - } - return parts, nil -} - -// CompareDeclarativeErrorsAndEmitMismatches checks for mismatches between imperative and declarative validation -// and logs + emits metrics when inconsistencies are found -func CompareDeclarativeErrorsAndEmitMismatches(ctx context.Context, imperativeErrs, declarativeErrs field.ErrorList, takeover bool) { - logger := klog.FromContext(ctx) - mismatchDetails := gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs, takeover) - for _, detail := range mismatchDetails { - // Log information about the mismatch using contextual logger - logger.Error(nil, detail) - - // Increment the metric for the mismatch - validationmetrics.Metrics.IncDeclarativeValidationMismatchMetric() - } -} - -// gatherDeclarativeValidationMismatches compares imperative and declarative validation errors -// and returns detailed information about any mismatches found. Errors are compared via type, field, and origin -func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field.ErrorList, takeover bool) []string { - var mismatchDetails []string - // short circuit here to minimize allocs for usual case of 0 validation errors - if len(imperativeErrs) == 0 && len(declarativeErrs) == 0 { - return mismatchDetails - } - // recommendation based on takeover status - recommendation := "This difference should not affect system operation since hand written validation is authoritative." - if takeover { - recommendation = "Consider disabling the DeclarativeValidationTakeover feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes." - } - fuzzyMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin().RequireOriginWhenInvalid() - exactMatcher := field.ErrorMatcher{}.Exactly() - - // Dedupe imperative errors of exact error matches as they are - // not intended and come from (buggy) duplicate validation calls - // This is necessary as without deduping we could get unmatched - // imperative errors for cases that are correct (matching) - dedupedImperativeErrs := field.ErrorList{} - for _, err := range imperativeErrs { - found := false - for _, existingErr := range dedupedImperativeErrs { - if exactMatcher.Matches(existingErr, err) { - found = true - break - } - } - if !found { - dedupedImperativeErrs = append(dedupedImperativeErrs, err) - } - } - imperativeErrs = dedupedImperativeErrs - - // Create a copy of declarative errors to track remaining ones - remaining := make(field.ErrorList, len(declarativeErrs)) - copy(remaining, declarativeErrs) - - // Match each "covered" imperative error to declarative errors. - // We use a fuzzy matching approach to find corresponding declarative errors - // for each imperative error marked as CoveredByDeclarative. - // As matches are found, they're removed from the 'remaining' list. - // They are removed from `remaining` with a "1:many" mapping: for a given - // imperative error we mark as matched all matching declarative errors - // This allows us to: - // 1. Detect imperative errors that should have matching declarative errors but don't - // 2. Identify extra declarative errors with no imperative counterpart - // Both cases indicate issues with the declarative validation implementation. - for _, iErr := range imperativeErrs { - if !iErr.CoveredByDeclarative { - continue - } - - tmp := make(field.ErrorList, 0, len(remaining)) - matchCount := 0 - - for _, dErr := range remaining { - if fuzzyMatcher.Matches(iErr, dErr) { - matchCount++ - } else { - tmp = append(tmp, dErr) - } - } - - if matchCount == 0 { - mismatchDetails = append(mismatchDetails, - fmt.Sprintf( - "Unexpected difference between hand written validation and declarative validation error results, unmatched error(s) found %s. "+ - "This indicates an issue with declarative validation. %s", - fuzzyMatcher.Render(iErr), - recommendation, - ), - ) - } - - remaining = tmp - } - - // Any remaining unmatched declarative errors are considered "extra" - for _, dErr := range remaining { - mismatchDetails = append(mismatchDetails, - fmt.Sprintf( - "Unexpected difference between hand written validation and declarative validation error results, extra error(s) found %s. "+ - "This indicates an issue with declarative validation. %s", - fuzzyMatcher.Render(dErr), - recommendation, - ), - ) - } - - return mismatchDetails -} - -// createDeclarativeValidationPanicHandler returns a function with panic recovery logic -// that will increment the panic metric and either log or append errors based on the takeover parameter. -func createDeclarativeValidationPanicHandler(ctx context.Context, errs *field.ErrorList, takeover bool) func() { - logger := klog.FromContext(ctx) - return func() { - if r := recover(); r != nil { - // Increment the panic metric counter - validationmetrics.Metrics.IncDeclarativeValidationPanicMetric() - - const errorFmt = "panic during declarative validation: %v" - if takeover { - // If takeover is enabled, output as a validation error as authoritative validator panicked and validation should error - *errs = append(*errs, field.InternalError(nil, fmt.Errorf(errorFmt, r))) - } else { - // if takeover not enabled, log the panic as an error message - logger.Error(nil, fmt.Sprintf(errorFmt, r)) - } - } - } -} - -// panicSafeValidateFunc wraps an validation function with panic recovery logic. -// The returned function will execute the wrapped function and handle any panics by -// incrementing the panic metric, and logging an error message -// if takeover=false, and adding a validation error if takeover=true. -func panicSafeValidateFunc( - validateUpdateFunc func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList, - takeover bool, -) func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList { - return func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) (errs field.ErrorList) { - defer createDeclarativeValidationPanicHandler(ctx, &errs, takeover)() - - return validateUpdateFunc(ctx, scheme, obj, oldObj, o) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate_test.go deleted file mode 100644 index 194471929..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/validate_test.go +++ /dev/null @@ -1,692 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rest - -import ( - "bytes" - "context" - "fmt" - "reflect" - "regexp" - "slices" - "strings" - "testing" - - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/operation" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/conversion" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - genericapirequest "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/klog/v2" -) - -func TestValidateDeclaratively(t *testing.T) { - valid := &Pod{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Pod", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - } - - invalidRestartPolicy := &Pod{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Pod", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - RestartPolicy: "INVALID", - } - - invalidRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid value").WithOrigin("invalid-test") - mutatedRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Immutable field").WithOrigin("immutable-test") - invalidStatusErr := field.Invalid(field.NewPath("status", "conditions"), "", "Invalid condition").WithOrigin("invalid-condition") - invalidIfOptionErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set") - invalidSubresourceErr := field.InternalError(nil, fmt.Errorf("unexpected error parsing subresource path: %w", fmt.Errorf("invalid subresource path: %s", "invalid/status"))) - - testCases := []struct { - name string - object runtime.Object - oldObject runtime.Object - subresource string - options []string - expected field.ErrorList - }{ - { - name: "create", - object: invalidRestartPolicy, - expected: field.ErrorList{invalidRestartPolicyErr}, - }, - { - name: "update", - object: invalidRestartPolicy, - oldObject: valid, - expected: field.ErrorList{invalidRestartPolicyErr, mutatedRestartPolicyErr}, - }, - { - name: "update subresource with declarative validation", - subresource: "status", - object: valid, - oldObject: valid, - expected: field.ErrorList{invalidStatusErr}, - }, - { - name: "update subresource without declarative validation", - subresource: "scale", - object: valid, - oldObject: valid, - expected: field.ErrorList{}, // Expect no errors if there is no registered validation - }, - { - name: "invalid subresource", - subresource: "/invalid/status", - object: valid, - oldObject: valid, - expected: field.ErrorList{invalidSubresourceErr}, - }, - { - name: "update with option", - options: []string{"option1"}, - object: valid, - expected: field.ErrorList{invalidIfOptionErr}, - }, - } - - ctx := context.Background() - - internalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal} - v1GV := schema.GroupVersion{Group: "", Version: "v1"} - - scheme := runtime.NewScheme() - scheme.AddKnownTypes(internalGV, &Pod{}) - scheme.AddKnownTypes(v1GV, &v1.Pod{}) - - scheme.AddValidationFunc(&v1.Pod{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList { - results := field.ErrorList{} - if op.HasOption("option1") { - results = append(results, invalidIfOptionErr) - } - if slices.Equal(op.Request.Subresources, []string{"status"}) { - results = append(results, invalidStatusErr) - } - if op.Type == operation.Update && object.(*v1.Pod).Spec.RestartPolicy != oldObject.(*v1.Pod).Spec.RestartPolicy { - results = append(results, mutatedRestartPolicyErr) - } - if object.(*v1.Pod).Spec.RestartPolicy == "INVALID" { - results = append(results, invalidRestartPolicyErr) - } - return results - }) - err := scheme.AddConversionFunc(&Pod{}, &v1.Pod{}, func(a, b interface{}, scope conversion.Scope) error { - if in, ok := a.(*Pod); ok { - if out, ok := b.(*v1.Pod); ok { - out.APIVersion = in.APIVersion - out.Kind = in.Kind - out.Spec.RestartPolicy = v1.RestartPolicy(in.RestartPolicy) - } - } - return nil - }) - if err != nil { - t.Fatal(err) - } - - for _, tc := range testCases { - ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{ - APIGroup: "", - APIVersion: "v1", - Subresource: tc.subresource, - }) - t.Run(tc.name, func(t *testing.T) { - var results field.ErrorList - if tc.oldObject == nil { - results = ValidateDeclaratively(ctx, scheme, tc.object, WithOptions(tc.options)) - } else { - results = ValidateUpdateDeclaratively(ctx, scheme, tc.object, tc.oldObject, WithOptions(tc.options)) - } - matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() - matcher.Test(t, tc.expected, results) - }) - } -} - -// Fake internal pod type, since core.Pod cannot be imported by this package -type Pod struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - RestartPolicy string `json:"restartPolicy"` -} - -func (Pod) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } - -func (p Pod) DeepCopyObject() runtime.Object { - return &Pod{ - TypeMeta: metav1.TypeMeta{ - APIVersion: p.APIVersion, - Kind: p.Kind, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: p.Name, - Namespace: p.Namespace, - }, - RestartPolicy: p.RestartPolicy, - } -} - -// TestGatherDeclarativeValidationMismatches tests all mismatch -// scenarios across imperative and declarative errors for -// the gatherDeclarativeValidationMismatches function -func TestGatherDeclarativeValidationMismatches(t *testing.T) { - replicasPath := field.NewPath("spec").Child("replicas") - minReadySecondsPath := field.NewPath("spec").Child("minReadySeconds") - selectorPath := field.NewPath("spec").Child("selector") - - errA := field.Invalid(replicasPath, nil, "regular error A") - errB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") - coveredErrB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") - errBWithDiffDetail := field.Invalid(minReadySecondsPath, -1, "covered error B - different detail").WithOrigin("minimum") - coveredErrB.CoveredByDeclarative = true - errC := field.Invalid(replicasPath, nil, "covered error C").WithOrigin("minimum") - coveredErrC := field.Invalid(replicasPath, nil, "covered error C").WithOrigin("minimum") - coveredErrC.CoveredByDeclarative = true - errCWithDiffOrigin := field.Invalid(replicasPath, nil, "covered error C").WithOrigin("maximum") - errD := field.Invalid(selectorPath, nil, "regular error D") - - testCases := []struct { - name string - imperativeErrors field.ErrorList - declarativeErrors field.ErrorList - takeover bool - expectMismatches bool - expectDetailsContaining []string - }{ - { - name: "Declarative and imperative return 0 errors - no mismatch", - imperativeErrors: field.ErrorList{}, - declarativeErrors: field.ErrorList{}, - takeover: false, - expectMismatches: false, - expectDetailsContaining: []string{}, - }, - { - name: "Declarative returns multiple errors with different origins, errors match - no mismatch", - imperativeErrors: field.ErrorList{ - errA, - coveredErrB, - coveredErrC, - errD, - }, - declarativeErrors: field.ErrorList{ - errB, - errC, - }, - takeover: false, - expectMismatches: false, - expectDetailsContaining: []string{}, - }, - { - name: "Declarative returns multiple errors with different origins, errors don't match - mismatch case", - imperativeErrors: field.ErrorList{ - errA, - coveredErrB, - coveredErrC, - }, - declarativeErrors: field.ErrorList{ - errB, - errCWithDiffOrigin, - }, - takeover: true, - expectMismatches: true, - expectDetailsContaining: []string{ - "Unexpected difference between hand written validation and declarative validation error results", - "unmatched error(s) found", - "extra error(s) found", - "replicas", - "Consider disabling the DeclarativeValidationTakeover feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes", - }, - }, - { - name: "Declarative and imperative return exactly 1 error, errors match - no mismatch", - imperativeErrors: field.ErrorList{ - coveredErrB, - }, - declarativeErrors: field.ErrorList{ - errB, - }, - takeover: false, - expectMismatches: false, - expectDetailsContaining: []string{}, - }, - { - name: "Declarative and imperative exactly 1 error, errors don't match - mismatch", - imperativeErrors: field.ErrorList{ - coveredErrB, - }, - declarativeErrors: field.ErrorList{ - errC, - }, - takeover: false, - expectMismatches: true, - expectDetailsContaining: []string{ - "Unexpected difference between hand written validation and declarative validation error results", - "unmatched error(s) found", - "minReadySeconds", - "extra error(s) found", - "replicas", - "This difference should not affect system operation since hand written validation is authoritative", - }, - }, - { - name: "Declarative returns 0 errors, imperative returns 1 covered error - mismatch", - imperativeErrors: field.ErrorList{ - coveredErrB, - }, - declarativeErrors: field.ErrorList{}, - takeover: true, - expectMismatches: true, - expectDetailsContaining: []string{ - "Unexpected difference between hand written validation and declarative validation error results", - "unmatched error(s) found", - "minReadySeconds", - "Consider disabling the DeclarativeValidationTakeover feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes", - }, - }, - { - name: "Declarative returns 0 errors, imperative returns 1 uncovered error - no mismatch", - imperativeErrors: field.ErrorList{ - errB, - }, - declarativeErrors: field.ErrorList{}, - takeover: false, - expectMismatches: false, - expectDetailsContaining: []string{}, - }, - { - name: "Declarative returns 1 error, imperative returns 0 error - mismatch", - imperativeErrors: field.ErrorList{}, - declarativeErrors: field.ErrorList{ - errB, - }, - takeover: false, - expectMismatches: true, - expectDetailsContaining: []string{ - "Unexpected difference between hand written validation and declarative validation error results", - "extra error(s) found", - "minReadySeconds", - "This difference should not affect system operation since hand written validation is authoritative", - }, - }, - { - name: "Declarative returns 1 error, imperative returns 3 matching errors - no mismatch", - imperativeErrors: field.ErrorList{ - coveredErrB, - }, - declarativeErrors: field.ErrorList{ - errB, - errB, - errBWithDiffDetail, - }, - takeover: false, - expectMismatches: false, - expectDetailsContaining: []string{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - details := gatherDeclarativeValidationMismatches(tc.imperativeErrors, tc.declarativeErrors, tc.takeover) - // Check if mismatches were found if expected - if tc.expectMismatches && len(details) == 0 { - t.Errorf("Expected mismatches but got none") - } - // Check if details contain expected text - detailsStr := strings.Join(details, " ") - for _, expectedContent := range tc.expectDetailsContaining { - if !strings.Contains(detailsStr, expectedContent) { - t.Errorf("Expected details to contain: %q, but they didn't.\nDetails were:\n%s", - expectedContent, strings.Join(details, "\n")) - } - } - // If we don't expect any details, make sure none provided - if len(tc.expectDetailsContaining) == 0 && len(details) > 0 { - t.Errorf("Expected no details, but got %d details: %v", len(details), details) - } - }) - } -} - -// TestCompareDeclarativeErrorsAndEmitMismatches tests expected -// logging of mismatch information given match & mismatch error conditions. -func TestCompareDeclarativeErrorsAndEmitMismatches(t *testing.T) { - replicasPath := field.NewPath("spec").Child("replicas") - minReadySecondsPath := field.NewPath("spec").Child("minReadySeconds") - - errA := field.Invalid(replicasPath, nil, "regular error A") - errB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") - coveredErrB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") - coveredErrB.CoveredByDeclarative = true - - testCases := []struct { - name string - imperativeErrs field.ErrorList - declarativeErrs field.ErrorList - takeover bool - expectLogs bool - expectedRegex string - }{ - { - name: "mismatched errors, log info", - imperativeErrs: field.ErrorList{coveredErrB}, - declarativeErrs: field.ErrorList{errA}, - takeover: true, - expectLogs: true, - // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] - expectedRegex: "E.*Unexpected difference between hand written validation and declarative validation error results.*Consider disabling the DeclarativeValidationTakeover feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes", - }, - { - name: "matching errors, don't log info", - imperativeErrs: field.ErrorList{coveredErrB}, - declarativeErrs: field.ErrorList{errB}, - takeover: true, - expectLogs: false, - expectedRegex: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var buf bytes.Buffer - klog.SetOutput(&buf) - klog.LogToStderr(false) - defer klog.LogToStderr(true) - ctx := context.Background() - - CompareDeclarativeErrorsAndEmitMismatches(ctx, tc.imperativeErrs, tc.declarativeErrs, tc.takeover) - - klog.Flush() - logOutput := buf.String() - - if tc.expectLogs { - matched, err := regexp.MatchString(tc.expectedRegex, logOutput) - if err != nil { - t.Fatalf("Bad regex: %v", err) - } - if !matched { - t.Errorf("Expected log output to match %q, but got:\n%s", tc.expectedRegex, logOutput) - } - } else if len(logOutput) > 0 { - t.Errorf("Expected no mismatch logs, but found: %s", logOutput) - } - }) - } -} - -func TestWithRecover(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - var options []string - obj := &runtime.Unknown{} - - testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList - takeoverEnabled bool - wantErrs field.ErrorList - expectLogRegex string - }{ - { - name: "no panic", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - return field.ErrorList{ - field.Invalid(field.NewPath("field"), "value", "reason"), - } - }, - takeoverEnabled: false, - wantErrs: field.ErrorList{ - field.Invalid(field.NewPath("field"), "value", "reason"), - }, - expectLogRegex: "", - }, - { - name: "panic with takeover disabled", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - panic("test panic") - }, - takeoverEnabled: false, - wantErrs: nil, - // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] - expectLogRegex: "E.*panic during declarative validation: test panic", - }, - { - name: "panic with takeover enabled", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - panic("test panic") - }, - takeoverEnabled: true, - wantErrs: field.ErrorList{ - field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic")), - }, - expectLogRegex: "", - }, - { - name: "nil return, no panic", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - return nil - }, - takeoverEnabled: false, - wantErrs: nil, - expectLogRegex: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var buf bytes.Buffer - klog.SetOutput(&buf) - klog.LogToStderr(false) - defer klog.LogToStderr(true) - - // Pass the takeover flag to panicSafeValidateFunc instead of relying on the feature gate - wrapped := panicSafeValidateFunc(tc.validateFn, tc.takeoverEnabled) - gotErrs := wrapped(ctx, scheme, obj, nil, &validationConfigOption{opType: operation.Create, options: options, takeover: tc.takeoverEnabled}) - - klog.Flush() - logOutput := buf.String() - - // Compare gotErrs vs. tc.wantErrs - if !equalErrorLists(gotErrs, tc.wantErrs) { - t.Errorf("panicSafeValidateFunc() gotErrs = %#v, want %#v", gotErrs, tc.wantErrs) - } - - // Check logs if needed - if tc.expectLogRegex != "" { - matched, err := regexp.MatchString(tc.expectLogRegex, logOutput) - if err != nil { - t.Fatalf("Bad regex: %v", err) - } - if !matched { - t.Errorf("Expected log output %q, but got:\n%s", tc.expectLogRegex, logOutput) - } - } else if strings.Contains(logOutput, "panic during declarative validation") { - t.Errorf("Unexpected panic log found: %s", logOutput) - } - }) - } -} - -func TestWithRecoverUpdate(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - var options []string - obj := &runtime.Unknown{} - oldObj := &runtime.Unknown{} - - testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList - takeoverEnabled bool - wantErrs field.ErrorList - expectLogRegex string - }{ - { - name: "no panic", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - return field.ErrorList{ - field.Invalid(field.NewPath("field"), "value", "reason"), - } - }, - takeoverEnabled: false, - wantErrs: field.ErrorList{ - field.Invalid(field.NewPath("field"), "value", "reason"), - }, - expectLogRegex: "", - }, - { - name: "panic with takeover disabled", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - panic("test update panic") - }, - takeoverEnabled: false, - wantErrs: nil, - // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] - expectLogRegex: "E.*panic during declarative validation: test update panic", - }, - { - name: "panic with takeover enabled", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - panic("test update panic") - }, - takeoverEnabled: true, - wantErrs: field.ErrorList{ - field.InternalError(nil, fmt.Errorf("panic during declarative validation: test update panic")), - }, - expectLogRegex: "", - }, - { - name: "nil return, no panic", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { - return nil - }, - takeoverEnabled: false, - wantErrs: nil, - expectLogRegex: "", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - var buf bytes.Buffer - klog.SetOutput(&buf) - klog.LogToStderr(false) - defer klog.LogToStderr(true) - - // Pass the takeover flag to panicSafeValidateUpdateFunc instead of relying on the feature gate - wrapped := panicSafeValidateFunc(tc.validateFn, tc.takeoverEnabled) - gotErrs := wrapped(ctx, scheme, obj, oldObj, &validationConfigOption{opType: operation.Update, options: options, takeover: tc.takeoverEnabled}) - - klog.Flush() - logOutput := buf.String() - - // Compare gotErrs with wantErrs - if !equalErrorLists(gotErrs, tc.wantErrs) { - t.Errorf("panicSafeValidateUpdateFunc() gotErrs = %#v, want %#v", gotErrs, tc.wantErrs) - } - - // Verify log output - if tc.expectLogRegex != "" { - matched, err := regexp.MatchString(tc.expectLogRegex, logOutput) - if err != nil { - t.Fatalf("Bad regex: %v", err) - } - if !matched { - t.Errorf("Expected log pattern %q, but got:\n%s", tc.expectLogRegex, logOutput) - } - } else if strings.Contains(logOutput, "panic during declarative validation") { - t.Errorf("Unexpected panic log found: %s", logOutput) - } - }) - } -} - -func TestValidateDeclarativelyWithRecovery(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - var options []string - obj := &runtime.Unknown{} - - // Simple test for the ValidateDeclarativelyWithRecovery function - t.Run("with takeover disabled", func(t *testing.T) { - errs := ValidateDeclaratively(ctx, scheme, obj, WithOptions(options), WithTakeover(false)) - if errs == nil { - // This is expected to error since the request info is missing - t.Errorf("Expected errors but got nil") - } - }) - - t.Run("with takeover enabled", func(t *testing.T) { - errs := ValidateDeclaratively(ctx, scheme, obj, WithOptions(options), WithTakeover(true)) - if errs == nil { - // This is expected to error since the request info is missioptionsng - t.Errorf("Expected errors but got nil") - } - }) -} - -func TestValidateUpdateDeclarativelyWithRecovery(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - var options []string - obj := &runtime.Unknown{} - oldObj := &runtime.Unknown{} - - // Simple test for the ValidateUpdateDeclarativelyWithRecovery function - t.Run("with takeover disabled", func(t *testing.T) { - errs := ValidateUpdateDeclaratively(ctx, scheme, obj, oldObj, WithOptions(options), WithTakeover(false)) - if errs == nil { - // This is expected to error since the request info is missing - t.Errorf("Expected errors but got nil") - } - }) - - t.Run("with takeover enabled", func(t *testing.T) { - errs := ValidateUpdateDeclaratively(ctx, scheme, obj, oldObj, WithOptions(options), WithTakeover(true)) - if errs == nil { - // This is expected to error since the request info is missing - t.Errorf("Expected errors but got nil") - } - }) -} - -func equalErrorLists(a, b field.ErrorList) bool { - // If both are nil, consider them equal - if a == nil && b == nil { - return true - } - // If one is nil and the other not, they're different - if (a == nil && b != nil) || (a != nil && b == nil) { - return false - } - // Both non-nil: do a normal DeepEqual - return reflect.DeepEqual(a, b) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement_test.go deleted file mode 100644 index a14319e53..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement_test.go +++ /dev/null @@ -1,79 +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 options - -import ( - "strings" - "testing" - - utilerrors "k8s.io/apimachinery/pkg/util/errors" - cliflag "k8s.io/component-base/cli/flag" -) - -type fakeGroupRegistry struct{} - -func (f fakeGroupRegistry) IsGroupRegistered(group string) bool { - return group == "apiregistration.k8s.io" -} - -func TestAPIEnablementOptionsValidate(t *testing.T) { - testCases := []struct { - name string - runtimeConfig cliflag.ConfigurationMap - expectErr string - }{ - { - name: "test when options is nil", - }, - { - name: "test when invalid key with only api/all=false", - runtimeConfig: cliflag.ConfigurationMap{"api/all": "false"}, - expectErr: "invalid key with only api/all=false", - }, - { - name: "test when ConfigurationMap key is invalid", - runtimeConfig: cliflag.ConfigurationMap{"apiall": "false"}, - expectErr: "runtime-config invalid key", - }, - { - name: "test when unknown api groups", - runtimeConfig: cliflag.ConfigurationMap{"api/v1": "true"}, - expectErr: "unknown api groups", - }, - { - name: "test when valid api groups", - runtimeConfig: cliflag.ConfigurationMap{"apiregistration.k8s.io/v1beta1": "true"}, - }, - } - testGroupRegistry := fakeGroupRegistry{} - - for _, testcase := range testCases { - t.Run(testcase.name, func(t *testing.T) { - testOptions := &APIEnablementOptions{ - RuntimeConfig: testcase.runtimeConfig, - } - errs := testOptions.Validate(testGroupRegistry) - if len(testcase.expectErr) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), testcase.expectErr) { - t.Errorf("got err: %v, expected err: %s", errs, testcase.expectErr) - } - - if len(testcase.expectErr) == 0 && len(errs) != 0 { - t.Errorf("got err: %s, expected err nil", errs) - } - }) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback_test.go deleted file mode 100644 index c4b0c57b5..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback_test.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2021 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 options - -import ( - "net" - "testing" - - "k8s.io/apiserver/pkg/server" - "k8s.io/client-go/rest" - netutils "k8s.io/utils/net" -) - -func TestEmptyMainCert(t *testing.T) { - secureServingInfo := &server.SecureServingInfo{} - var loopbackClientConfig *rest.Config - - s := (&SecureServingOptions{ - BindAddress: netutils.ParseIPSloppy("127.0.0.1"), - }).WithLoopback() - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("failed to listen on 127.0.0.1:0") - } - defer ln.Close() - s.Listener = ln - s.BindPort = ln.Addr().(*net.TCPAddr).Port - - if err := s.ApplyTo(&secureServingInfo, &loopbackClientConfig); err != nil { - t.Errorf("unexpected error: %v", err) - } - if loopbackClientConfig == nil { - t.Errorf("unexpected empty loopbackClientConfig") - } - if e, a := 1, len(secureServingInfo.SNICerts); e != a { - t.Errorf("expected %d SNICert, got %d", e, a) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/openapi.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/openapi.go deleted file mode 100644 index 12c8b1ad9..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/openapi.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2016 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 routes - -import ( - restful "github.com/emicklei/go-restful/v3" - "k8s.io/klog/v2" - - "k8s.io/apiserver/pkg/server/mux" - builder2 "k8s.io/kube-openapi/pkg/builder" - "k8s.io/kube-openapi/pkg/builder3" - "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/common/restfuladapter" - "k8s.io/kube-openapi/pkg/handler" - "k8s.io/kube-openapi/pkg/handler3" - "k8s.io/kube-openapi/pkg/validation/spec" -) - -// OpenAPI installs spec endpoints for each web service. -type OpenAPI struct { - Config *common.Config - V3Config *common.OpenAPIV3Config -} - -// Install adds the SwaggerUI webservice to the given mux. -func (oa OpenAPI) InstallV2(c *restful.Container, mux *mux.PathRecorderMux) (*handler.OpenAPIService, *spec.Swagger) { - spec, err := builder2.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(c.RegisteredWebServices()), oa.Config) - if err != nil { - klog.Fatalf("Failed to build open api spec for root: %v", err) - } - spec.Definitions = handler.PruneDefaults(spec.Definitions) - openAPIVersionedService := handler.NewOpenAPIService(spec) - openAPIVersionedService.RegisterOpenAPIVersionedService("/openapi/v2", mux) - - return openAPIVersionedService, spec -} - -// InstallV3 adds the static group/versions defined in the RegisteredWebServices to the OpenAPI v3 spec -func (oa OpenAPI) InstallV3(c *restful.Container, mux *mux.PathRecorderMux) *handler3.OpenAPIService { - openAPIVersionedService := handler3.NewOpenAPIService() - err := openAPIVersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", mux) - if err != nil { - klog.Fatalf("Failed to register versioned open api spec for root: %v", err) - } - - grouped := make(map[string][]*restful.WebService) - - for _, t := range c.RegisteredWebServices() { - // Strip the "/" prefix from the name - gvName := t.RootPath()[1:] - grouped[gvName] = []*restful.WebService{t} - } - - for gv, ws := range grouped { - spec, err := builder3.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(ws), oa.V3Config) - if err != nil { - klog.Errorf("Failed to build OpenAPI v3 for group %s, %q", gv, err) - - } - openAPIVersionedService.UpdateGroupVersion(gv, spec) - } - return openAPIVersionedService -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config_test.go deleted file mode 100644 index a6984deed..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config_test.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -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 storage - -import ( - "testing" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - runtimetesting "k8s.io/apimachinery/pkg/runtime/testing" - "k8s.io/apimachinery/pkg/test" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - utilversion "k8s.io/apimachinery/pkg/util/version" - basecompatibility "k8s.io/component-base/compatibility" -) - -func TestEmulatedStorageVersion(t *testing.T) { - cases := []struct { - name string - scheme *runtime.Scheme - binaryVersion schema.GroupVersion - effectiveVersion basecompatibility.EffectiveVersion - want schema.GroupVersion - }{ - { - name: "pick compatible", - scheme: AlphaBetaScheme(utilversion.MustParse("1.31"), utilversion.MustParse("1.32")), - binaryVersion: v1beta1, - effectiveVersion: basecompatibility.NewEffectiveVersionFromString("1.32", "", ""), - want: v1alpha1, - }, - { - name: "alpha has been replaced, pick binary version", - scheme: AlphaReplacedBetaScheme(utilversion.MustParse("1.31"), utilversion.MustParse("1.32")), - binaryVersion: v1beta1, - effectiveVersion: basecompatibility.NewEffectiveVersionFromString("1.32", "", ""), - want: v1beta1, - }, - } - - for _, tc := range cases { - test.TestScheme() - t.Run(tc.name, func(t *testing.T) { - found, err := emulatedStorageVersion(tc.binaryVersion, &CronJob{}, tc.effectiveVersion, tc.scheme) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if found != tc.want { - t.Errorf("got %v; want %v", found, tc.want) - } - }) - } -} - -var internalGV = schema.GroupVersion{Group: "workload.example.com", Version: runtime.APIVersionInternal} -var v1alpha1 = schema.GroupVersion{Group: "workload.example.com", Version: "v1alpha1"} -var v1beta1 = schema.GroupVersion{Group: "workload.example.com", Version: "v1beta1"} - -type CronJobWithReplacement struct { - introduced *utilversion.Version - A string `json:"A,omitempty"` - B int `json:"B,omitempty"` -} - -func (*CronJobWithReplacement) GetObjectKind() schema.ObjectKind { panic("not implemented") } -func (*CronJobWithReplacement) DeepCopyObject() runtime.Object { - panic("not implemented") -} - -func (in *CronJobWithReplacement) APILifecycleIntroduced() (major, minor int) { - if in.introduced == nil { - return 0, 0 - } - return int(in.introduced.Major()), int(in.introduced.Minor()) -} - -func (in *CronJobWithReplacement) APILifecycleReplacement() schema.GroupVersionKind { - return schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"} -} - -type CronJob struct { - introduced *utilversion.Version - A string `json:"A,omitempty"` - B int `json:"B,omitempty"` -} - -func (*CronJob) GetObjectKind() schema.ObjectKind { panic("not implemented") } -func (*CronJob) DeepCopyObject() runtime.Object { - panic("not implemented") -} - -func (in *CronJob) APILifecycleIntroduced() (major, minor int) { - if in.introduced == nil { - return 0, 0 - } - return int(in.introduced.Major()), int(in.introduced.Minor()) -} - -func AlphaBetaScheme(alphaVersion, betaVersion *utilversion.Version) *runtime.Scheme { - s := runtime.NewScheme() - s.AddKnownTypes(internalGV, &CronJob{}) - s.AddKnownTypes(v1alpha1, &CronJob{introduced: alphaVersion}) - s.AddKnownTypes(v1beta1, &CronJob{introduced: betaVersion}) - s.AddKnownTypeWithName(internalGV.WithKind("CronJob"), &CronJob{}) - s.AddKnownTypeWithName(v1alpha1.WithKind("CronJob"), &CronJob{introduced: alphaVersion}) - s.AddKnownTypeWithName(v1beta1.WithKind("CronJob"), &CronJob{introduced: betaVersion}) - utilruntime.Must(runtimetesting.RegisterConversions(s)) - return s -} - -func AlphaReplacedBetaScheme(alphaVersion, betaVersion *utilversion.Version) *runtime.Scheme { - s := runtime.NewScheme() - s.AddKnownTypes(internalGV, &CronJob{}) - s.AddKnownTypes(v1alpha1, &CronJobWithReplacement{introduced: alphaVersion}) - s.AddKnownTypes(v1beta1, &CronJob{introduced: betaVersion}) - s.AddKnownTypeWithName(internalGV.WithKind("CronJob"), &CronJob{}) - s.AddKnownTypeWithName(v1alpha1.WithKind("CronJob"), &CronJobWithReplacement{introduced: alphaVersion}) - s.AddKnownTypeWithName(v1beta1.WithKind("CronJob"), &CronJob{introduced: betaVersion}) - utilruntime.Must(runtimetesting.RegisterConversions(s)) - return s -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/metrics/metrics.go deleted file mode 100644 index 9b7aee4ea..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/metrics/metrics.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2023 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package metrics - -import ( - "context" - "sync" - - "k8s.io/component-base/metrics" - "k8s.io/component-base/metrics/legacyregistry" -) - -const ( - subsystem = "apiserver" - statuscode = "code" -) - -var registerMetricsOnce sync.Once - -var ( - // peerProxiedRequestsTotal counts the number of requests that were proxied to a peer kube-apiserver. - peerProxiedRequestsTotal = metrics.NewCounterVec( - &metrics.CounterOpts{ - Subsystem: subsystem, - Name: "rerouted_request_total", - Help: "Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it", - StabilityLevel: metrics.ALPHA, - }, - []string{statuscode}, - ) -) - -func Register() { - registerMetricsOnce.Do(func() { - legacyregistry.MustRegister(peerProxiedRequestsTotal) - }) -} - -// Only used for tests. -func Reset() { - legacyregistry.Reset() -} - -// IncPeerProxiedRequest increments the # of proxied requests to peer kube-apiserver -func IncPeerProxiedRequest(ctx context.Context, status string) { - peerProxiedRequestsTotal.WithContext(ctx).WithLabelValues(status).Add(1) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery.go deleted file mode 100644 index 8ceef28bb..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package peerproxy - -import ( - "context" - "fmt" - "net/http" - "time" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/client-go/discovery" - "k8s.io/klog/v2" - - apidiscoveryv2 "k8s.io/api/apidiscovery/v2" - v1 "k8s.io/api/coordination/v1" - schema "k8s.io/apimachinery/pkg/runtime/schema" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - apirequest "k8s.io/apiserver/pkg/endpoints/request" - responsewriterutil "k8s.io/apiserver/pkg/util/responsewriter" -) - -const ( - controllerName = "peer-discovery-cache-sync" - maxRetries = 5 -) - -func (h *peerProxyHandler) RunPeerDiscoveryCacheSync(ctx context.Context, workers int) { - defer utilruntime.HandleCrash() - defer h.peerLeaseQueue.ShutDown() - defer func() { - err := h.apiserverIdentityInformer.Informer().RemoveEventHandler(h.leaseRegistration) - if err != nil { - klog.Warning("error removing leaseInformer eventhandler") - } - }() - - klog.Infof("Workers: %d", workers) - for i := 0; i < workers; i++ { - klog.Infof("Starting worker") - go wait.UntilWithContext(ctx, h.runWorker, time.Second) - } - <-ctx.Done() -} - -func (h *peerProxyHandler) enqueueLease(lease *v1.Lease) { - h.peerLeaseQueue.Add(lease.Name) -} - -func (h *peerProxyHandler) runWorker(ctx context.Context) { - for h.processNextElectionItem(ctx) { - } -} - -func (h *peerProxyHandler) processNextElectionItem(ctx context.Context) bool { - key, shutdown := h.peerLeaseQueue.Get() - if shutdown { - return false - } - defer h.peerLeaseQueue.Done(key) - - err := h.syncPeerDiscoveryCache(ctx) - h.handleErr(err, key) - return true -} - -func (h *peerProxyHandler) syncPeerDiscoveryCache(ctx context.Context) error { - var fetchDiscoveryErr error - // Rebuild the peer discovery cache from available leases. - leases, err := h.apiserverIdentityInformer.Lister().List(h.identityLeaseLabelSelector) - if err != nil { - utilruntime.HandleError(err) - return err - } - - newCache := map[string]map[schema.GroupVersionResource]bool{} - for _, l := range leases { - _, ok := h.isValidPeerIdentityLease(l) - if !ok { - continue - } - - discoveryInfo, err := h.fetchNewDiscoveryFor(ctx, l.Name, *l.Spec.HolderIdentity) - if err != nil { - fetchDiscoveryErr = err - } - - if discoveryInfo != nil { - newCache[l.Name] = discoveryInfo - } - } - - // Overwrite cache with new contents. - h.peerDiscoveryInfoCache.Store(newCache) - return fetchDiscoveryErr -} - -func (h *peerProxyHandler) fetchNewDiscoveryFor(ctx context.Context, serverID string, holderIdentity string) (map[schema.GroupVersionResource]bool, error) { - hostport, err := h.hostportInfo(serverID) - if err != nil { - return nil, fmt.Errorf("failed to get host port info from identity lease for server %s: %w", serverID, err) - } - - klog.V(4).Infof("Proxying an agg-discovery call from %s to %s", h.serverID, serverID) - servedResources := make(map[schema.GroupVersionResource]bool) - var discoveryErr error - var discoveryResponse *apidiscoveryv2.APIGroupDiscoveryList - discoveryPaths := []string{"/api", "/apis"} - for _, path := range discoveryPaths { - discoveryResponse, discoveryErr = h.aggregateDiscovery(ctx, path, hostport) - if err != nil { - klog.ErrorS(err, "error querying discovery endpoint for serverID", "path", path, "serverID", serverID) - continue - } - - for _, groupDiscovery := range discoveryResponse.Items { - groupName := groupDiscovery.Name - if groupName == "" { - groupName = "core" - } - - for _, version := range groupDiscovery.Versions { - for _, resource := range version.Resources { - gvr := schema.GroupVersionResource{Group: groupName, Version: version.Version, Resource: resource.Resource} - servedResources[gvr] = true - } - } - } - } - - klog.V(4).Infof("Agg discovery done successfully by %s for %s", h.serverID, serverID) - return servedResources, discoveryErr -} - -func (h *peerProxyHandler) aggregateDiscovery(ctx context.Context, path string, hostport string) (*apidiscoveryv2.APIGroupDiscoveryList, error) { - req, err := http.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, err - } - - apiServerUser := &user.DefaultInfo{ - Name: user.APIServerUser, - UID: user.APIServerUser, - Groups: []string{user.AllAuthenticated}, - } - - ctx = apirequest.WithUser(ctx, apiServerUser) - req = req.WithContext(ctx) - - req.Header.Add("Accept", discovery.AcceptV2) - - writer := responsewriterutil.NewInMemoryResponseWriter() - h.proxyRequestToDestinationAPIServer(req, writer, hostport) - if writer.RespCode() != http.StatusOK { - return nil, fmt.Errorf("discovery request failed with status: %d", writer.RespCode()) - } - - parsed := &apidiscoveryv2.APIGroupDiscoveryList{} - if err := runtime.DecodeInto(h.discoverySerializer.UniversalDecoder(), writer.Data(), parsed); err != nil { - return nil, fmt.Errorf("error decoding discovery response: %w", err) - } - - return parsed, nil -} - -// handleErr checks if an error happened and makes sure we will retry later. -func (h *peerProxyHandler) handleErr(err error, key string) { - if err == nil { - h.peerLeaseQueue.Forget(key) - return - } - - if h.peerLeaseQueue.NumRequeues(key) < maxRetries { - klog.Infof("Error syncing discovery for peer lease %v: %v", key, err) - h.peerLeaseQueue.AddRateLimited(key) - return - } - - h.peerLeaseQueue.Forget(key) - utilruntime.HandleError(err) - klog.Infof("Dropping lease %s out of the queue: %v", key, err) -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy.go deleted file mode 100644 index f682a6c65..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2023 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 peerproxy - -import ( - "context" - "fmt" - "net/http" - "strings" - "sync/atomic" - "time" - - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/apiserver/pkg/reconcilers" - "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/transport" - "k8s.io/client-go/util/workqueue" - "k8s.io/klog/v2" - - apidiscoveryv2 "k8s.io/api/apidiscovery/v2" - v1 "k8s.io/api/coordination/v1" - schema "k8s.io/apimachinery/pkg/runtime/schema" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - coordinationv1informers "k8s.io/client-go/informers/coordination/v1" -) - -// Local discovery cache needs to be refreshed periodically to store -// updates made to custom resources or aggregated resource that can -// change dynamically. -const localDiscoveryRefreshInterval = 30 * time.Minute - -// Interface defines how the Mixed Version Proxy filter interacts with the underlying system. -type Interface interface { - WrapHandler(handler http.Handler) http.Handler - WaitForCacheSync(stopCh <-chan struct{}) error - HasFinishedSync() bool - RunLocalDiscoveryCacheSync(stopCh <-chan struct{}) error - RunPeerDiscoveryCacheSync(ctx context.Context, workers int) -} - -// New creates a new instance to implement unknown version proxy -// This method is used for an alpha feature UnknownVersionInteroperabilityProxy -// and is subject to future modifications. -func NewPeerProxyHandler( - serverId string, - identityLeaseLabelSelector string, - leaseInformer coordinationv1informers.LeaseInformer, - reconciler reconcilers.PeerEndpointLeaseReconciler, - ser runtime.NegotiatedSerializer, - loopbackClientConfig *rest.Config, - proxyClientConfig *transport.Config, -) (*peerProxyHandler, error) { - h := &peerProxyHandler{ - name: "PeerProxyHandler", - serverID: serverId, - reconciler: reconciler, - serializer: ser, - localDiscoveryInfoCache: atomic.Value{}, - localDiscoveryCacheTicker: time.NewTicker(localDiscoveryRefreshInterval), - localDiscoveryInfoCachePopulated: make(chan struct{}), - peerDiscoveryInfoCache: atomic.Value{}, - peerLeaseQueue: workqueue.NewTypedRateLimitingQueueWithConfig( - workqueue.DefaultTypedControllerRateLimiter[string](), - workqueue.TypedRateLimitingQueueConfig[string]{ - Name: controllerName, - }), - apiserverIdentityInformer: leaseInformer, - } - - if parts := strings.Split(identityLeaseLabelSelector, "="); len(parts) != 2 { - return nil, fmt.Errorf("invalid identityLeaseLabelSelector provided, must be of the form key=value, received: %v", identityLeaseLabelSelector) - } - selector, err := labels.Parse(identityLeaseLabelSelector) - if err != nil { - return nil, fmt.Errorf("failed to parse label selector: %w", err) - } - h.identityLeaseLabelSelector = selector - - discoveryScheme := runtime.NewScheme() - utilruntime.Must(apidiscoveryv2.AddToScheme(discoveryScheme)) - h.discoverySerializer = serializer.NewCodecFactory(discoveryScheme) - - discoveryClient, err := discovery.NewDiscoveryClientForConfig(loopbackClientConfig) - if err != nil { - return nil, fmt.Errorf("error creating discovery client: %w", err) - } - h.discoveryClient = discoveryClient - h.localDiscoveryInfoCache.Store(map[schema.GroupVersionResource]bool{}) - h.peerDiscoveryInfoCache.Store(map[string]map[schema.GroupVersionResource]bool{}) - - proxyTransport, err := transport.New(proxyClientConfig) - if err != nil { - return nil, fmt.Errorf("failed to create proxy transport: %w", err) - } - h.proxyTransport = proxyTransport - - peerDiscoveryRegistration, err := h.apiserverIdentityInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - if lease, ok := h.isValidPeerIdentityLease(obj); ok { - h.enqueueLease(lease) - } - }, - UpdateFunc: func(oldObj, newObj interface{}) { - oldLease, oldLeaseOk := h.isValidPeerIdentityLease(oldObj) - newLease, newLeaseOk := h.isValidPeerIdentityLease(newObj) - if oldLeaseOk && newLeaseOk && - oldLease.Name == newLease.Name && *oldLease.Spec.HolderIdentity != *newLease.Spec.HolderIdentity { - h.enqueueLease(newLease) - } - }, - DeleteFunc: func(obj interface{}) { - if lease, ok := h.isValidPeerIdentityLease(obj); ok { - h.enqueueLease(lease) - } - }, - }) - if err != nil { - return nil, err - } - - h.leaseRegistration = peerDiscoveryRegistration - return h, nil -} - -func (h *peerProxyHandler) isValidPeerIdentityLease(obj interface{}) (*v1.Lease, bool) { - lease, ok := obj.(*v1.Lease) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %T", obj)) - return nil, false - } - if lease, ok = tombstone.Obj.(*v1.Lease); !ok { - utilruntime.HandleError(fmt.Errorf("unexpected object type: %T", obj)) - return nil, false - } - } - - if lease == nil { - klog.Error(fmt.Errorf("nil lease object provided")) - return nil, false - } - - if h.identityLeaseLabelSelector != nil && h.identityLeaseLabelSelector.String() != "" { - identityLeaseLabel := strings.Split(h.identityLeaseLabelSelector.String(), "=") - if len(identityLeaseLabel) != 2 { - klog.Errorf("invalid identityLeaseLabelSelector format: %s", h.identityLeaseLabelSelector.String()) - return nil, false - } - - if lease.Labels == nil || lease.Labels[identityLeaseLabel[0]] != identityLeaseLabel[1] { - klog.V(4).Infof("lease %s/%s does not match label selector: %s=%s", lease.Namespace, lease.Name, identityLeaseLabel[0], identityLeaseLabel[1]) - return nil, false - } - - } - - // Ignore self. - if lease.Name == h.serverID { - return nil, false - } - - if lease.Spec.HolderIdentity == nil { - klog.Error(fmt.Errorf("invalid lease object provided, missing holderIdentity in lease obj")) - return nil, false - } - - return lease, true -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/wsstream/legacy.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/util/wsstream/legacy.go deleted file mode 100644 index 61b4dd489..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/wsstream/legacy.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2023 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. -*/ - -// Deprecated: This WebSockets package under apiserver is no longer in use. -// Please use the apimachinery version of the package at: -// -// k8s.io/apimachinery/pkg/util/httpstream/wsstream -package wsstream - -import apimachinerywsstream "k8s.io/apimachinery/pkg/util/httpstream/wsstream" - -// Aliases for all exported symbols previously in "conn.go" -const ( - ChannelWebSocketProtocol = apimachinerywsstream.ChannelWebSocketProtocol - Base64ChannelWebSocketProtocol = apimachinerywsstream.Base64ChannelWebSocketProtocol -) - -type ChannelType = apimachinerywsstream.ChannelType - -const ( - IgnoreChannel = apimachinerywsstream.IgnoreChannel - ReadChannel = apimachinerywsstream.ReadChannel - WriteChannel = apimachinerywsstream.WriteChannel - ReadWriteChannel = apimachinerywsstream.ReadWriteChannel -) - -type ChannelProtocolConfig = apimachinerywsstream.ChannelProtocolConfig - -var ( - IsWebSocketRequest = apimachinerywsstream.IsWebSocketRequest - IgnoreReceives = apimachinerywsstream.IgnoreReceives - NewDefaultChannelProtocols = apimachinerywsstream.NewDefaultChannelProtocols -) - -type Conn = apimachinerywsstream.Conn - -var NewConn = apimachinerywsstream.NewConn - -// Aliases for all exported symbols previously in "stream.go" -type ReaderProtocolConfig = apimachinerywsstream.ReaderProtocolConfig - -var NewDefaultReaderProtocols = apimachinerywsstream.NewDefaultReaderProtocols - -type Reader = apimachinerywsstream.Reader - -var NewReader = apimachinerywsstream.NewReader diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics_test.go b/third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics_test.go deleted file mode 100644 index e93118a00..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics_test.go +++ /dev/null @@ -1,150 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package validation - -import ( - "strings" - "testing" - - "k8s.io/component-base/metrics/legacyregistry" - "k8s.io/component-base/metrics/testutil" -) - -// TestDeclarativeValidationMismatchMetric tests that the mismatch metric correctly increments once -func TestDeclarativeValidationMismatchMetric(t *testing.T) { - defer legacyregistry.Reset() - defer ResetValidationMetricsInstance() - - // Increment the metric once - Metrics.IncDeclarativeValidationMismatchMetric() - - expected := ` - # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. - # TYPE apiserver_validation_declarative_validation_mismatch_total counter - apiserver_validation_declarative_validation_mismatch_total 1 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_mismatch_total"); err != nil { - t.Fatal(err) - } -} - -// TestDeclarativeValidationPanicMetric tests that the panic metric correctly increments once -func TestDeclarativeValidationPanicMetric(t *testing.T) { - defer legacyregistry.Reset() - defer ResetValidationMetricsInstance() - - // Increment the metric once - Metrics.IncDeclarativeValidationPanicMetric() - - expected := ` - # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. - # TYPE apiserver_validation_declarative_validation_panic_total counter - apiserver_validation_declarative_validation_panic_total 1 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_panic_total"); err != nil { - t.Fatal(err) - } -} - -// TestDeclarativeValidationMismatchMetricMultiple tests that the mismatch metric correctly increments multiple times -func TestDeclarativeValidationMismatchMetricMultiple(t *testing.T) { - defer legacyregistry.Reset() - defer ResetValidationMetricsInstance() - - // Increment the metric three times - Metrics.IncDeclarativeValidationMismatchMetric() - Metrics.IncDeclarativeValidationMismatchMetric() - Metrics.IncDeclarativeValidationMismatchMetric() - - expected := ` - # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. - # TYPE apiserver_validation_declarative_validation_mismatch_total counter - apiserver_validation_declarative_validation_mismatch_total 3 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_mismatch_total"); err != nil { - t.Fatal(err) - } -} - -// TestDeclarativeValidationPanicMetricMultiple tests that the panic metric correctly increments multiple times -func TestDeclarativeValidationPanicMetricMultiple(t *testing.T) { - defer legacyregistry.Reset() - defer ResetValidationMetricsInstance() - - // Increment the metric three times - Metrics.IncDeclarativeValidationPanicMetric() - Metrics.IncDeclarativeValidationPanicMetric() - Metrics.IncDeclarativeValidationPanicMetric() - - expected := ` - # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. - # TYPE apiserver_validation_declarative_validation_panic_total counter - apiserver_validation_declarative_validation_panic_total 3 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_panic_total"); err != nil { - t.Fatal(err) - } -} - -// TestDeclarativeValidationMetricsReset tests that the Reset function correctly resets the metrics to zero -func TestDeclarativeValidationMetricsReset(t *testing.T) { - defer legacyregistry.Reset() - defer ResetValidationMetricsInstance() - - // Increment both metrics - Metrics.IncDeclarativeValidationMismatchMetric() - Metrics.IncDeclarativeValidationPanicMetric() - - // Reset the metrics - Metrics.Reset() - - // Verify they've been reset to zero - expected := ` - # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. - # TYPE apiserver_validation_declarative_validation_mismatch_total counter - apiserver_validation_declarative_validation_mismatch_total 0 - # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. - # TYPE apiserver_validation_declarative_validation_panic_total counter - apiserver_validation_declarative_validation_panic_total 0 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_mismatch_total", "declarative_validation_panic_total"); err != nil { - t.Fatal(err) - } - - // Increment the metrics again to ensure they're still functional - Metrics.IncDeclarativeValidationMismatchMetric() - Metrics.IncDeclarativeValidationPanicMetric() - - // Verify they've been incremented correctly - expected = ` - # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. - # TYPE apiserver_validation_declarative_validation_mismatch_total counter - apiserver_validation_declarative_validation_mismatch_total 1 - # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. - # TYPE apiserver_validation_declarative_validation_panic_total counter - apiserver_validation_declarative_validation_panic_total 1 - ` - - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "declarative_validation_mismatch_total", "declarative_validation_panic_total"); err != nil { - t.Fatal(err) - } -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics.go b/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics.go deleted file mode 100644 index 109cde254..000000000 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -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 oidc - -import ( - "context" - "crypto/sha256" - "fmt" - "sync" - "time" - - "k8s.io/apiserver/pkg/authentication/authenticator" - "k8s.io/component-base/metrics" - "k8s.io/component-base/metrics/legacyregistry" - "k8s.io/utils/clock" -) - -const ( - namespace = "apiserver" - subsystem = "authentication" -) - -var ( - jwtAuthenticatorLatencyMetric = metrics.NewHistogramVec( - &metrics.HistogramOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "jwt_authenticator_latency_seconds", - Help: "Latency of jwt authentication operations in seconds. This is the time spent authenticating a token for cache miss only (i.e. when the token is not found in the cache).", - StabilityLevel: metrics.ALPHA, - // default histogram buckets with a 1ms starting point - Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, - }, - []string{"result", "jwt_issuer_hash"}, - ) -) - -var registerMetrics sync.Once - -func RegisterMetrics() { - registerMetrics.Do(func() { - legacyregistry.MustRegister(jwtAuthenticatorLatencyMetric) - }) -} - -func recordAuthenticationLatency(result, jwtIssuerHash string, duration time.Duration) { - jwtAuthenticatorLatencyMetric.WithLabelValues(result, jwtIssuerHash).Observe(duration.Seconds()) -} - -func getHash(data string) string { - if len(data) > 0 { - return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(data))) - } - return "" -} - -func newInstrumentedAuthenticator(jwtIssuer string, delegate AuthenticatorTokenWithHealthCheck) AuthenticatorTokenWithHealthCheck { - return newInstrumentedAuthenticatorWithClock(jwtIssuer, delegate, clock.RealClock{}) -} - -func newInstrumentedAuthenticatorWithClock(jwtIssuer string, delegate AuthenticatorTokenWithHealthCheck, clock clock.PassiveClock) *instrumentedAuthenticator { - RegisterMetrics() - return &instrumentedAuthenticator{ - jwtIssuerHash: getHash(jwtIssuer), - delegate: delegate, - clock: clock, - } -} - -type instrumentedAuthenticator struct { - jwtIssuerHash string - delegate AuthenticatorTokenWithHealthCheck - clock clock.PassiveClock -} - -func (a *instrumentedAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { - start := a.clock.Now() - response, ok, err := a.delegate.AuthenticateToken(ctx, token) - // this only happens when issuer doesn't match the authenticator - // we don't want to record metrics for this case - if !ok && err == nil { - return response, ok, err - } - - duration := a.clock.Since(start) - if err != nil { - recordAuthenticationLatency("failure", a.jwtIssuerHash, duration) - } else { - recordAuthenticationLatency("success", a.jwtIssuerHash, duration) - } - return response, ok, err -} - -func (a *instrumentedAuthenticator) HealthCheck() error { - return a.delegate.HealthCheck() -} diff --git a/third_party/k8s.io/apiserver-v0.36.1/ARCHITECTURE.md b/third_party/k8s.io/apiserver-v0.36.1/ARCHITECTURE.md new file mode 100644 index 000000000..e9b8af683 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/ARCHITECTURE.md @@ -0,0 +1,259 @@ +# apiserver Architecture + +## 1. Server Composition + +The `kube-apiserver` binary is not one server, but a **server chain** of three distinct +`GenericAPIServer` instances: the core Kubernetes API, API extensions (CRDs), and the aggregation +layer. + +This composition is managed by a layered configuration system, starting with command-line +flags parsed by `options` structs (e.g., `RecommendedOptions` in `pkg/server/options`), which +populate a `Config` object that is then used to instantiate the `GenericAPIServer` +instances. The construction of this delegation chain can be found in the `CreateServerChain` +function in `cmd/kube-apiserver/app/server.go`. + +```mermaid +graph TD + subgraph Incoming Request + direction LR + A[User/Client] --> B{/apis/apps/v1/deployments}; + end + + subgraph kube-apiserver process + direction LR + B --> C[Aggregator Server]; + C -- Not an APIService --> D[Kube API Server]; + D -- Handles Request --> E[REST Storage]; + C -- Is an APIService --> F[Proxy to Extension API Server]; + D -- Not a Core API --> G[API Extensions Server]; + G -- Handles CRD --> E; + end +``` + +1. **Aggregator Server (`kube-aggregator`):** + * **Purpose:** Handles the `apiregistration.k8s.io` API and acts as a reverse proxy for + extension API servers. This functionality was designed to allow third-party APIs to be + "aggregated" into the main Kubernetes API server seamlessly. + * **Mechanism:** It watches `APIService` objects. When a request arrives (e.g., for + `/apis/mycompany.com/v1/myresources`), it checks if an `APIService` has "claimed" + that path. If so, it uses a `ServiceResolver` to find the IP of the backing + `Service` and proxies the request. + * **Use Case:** This pattern is for programmatic, high-control extensions that require + custom business logic (e.g., non-CRUD subresources like `/logs`) or alternative + storage backends. + * **Delegation:** If no `APIService` matches, it delegates the request to the next + server in the chain. + +2. **Kube API Server (Core):** + * **Purpose:** Serves all the built-in Kubernetes APIs (`core/v1`, `apps/v1`, etc.). + * **Mechanism:** This is the main server, configured with all the core REST storage + strategies. + * **Delegation:** If a request is for a path that is not a core API (e.g., for a CRD), + it delegates the request to the next server in the chain. + +3. **API Extensions Server (`apiextensions-apiserver`):** + * **Purpose:** Handles the `apiextensions.k8s.io` API, which manages + `CustomResourceDefinition` objects (CRDs). The evolution of CRDs from a simple extension + mechanism to a feature-rich system with validation, versioning, and defaulting is + documented in a series of KEPs, starting with the graduation to GA in Kubernetes v1.16. + * **Mechanism:** When a CRD is created, this server dynamically creates and installs a + new REST storage handler for the new resource, making it immediately available. + * **Use Case:** CRDs are the most common extension pattern, offering a declarative, + schema-based way to define new resource types that are stored in etcd and require no + custom API server code. + * **Delegation:** It is the end of the chain. If it cannot handle a request, a `404 Not + Found` is returned. + +## 2. Handler Chain + +Every request flows through a standard chain of HTTP handlers (filters). The request body +is not deserialized until it has passed authentication and authorization. The default handler +chain is constructed by the `DefaultBuildHandlerChain` function in +`staging/src/k8s.io/apiserver/pkg/server/config.go`. + +```mermaid +sequenceDiagram + participant Client + participant Handler Chain + participant Authentication + participant Authorization + participant Priority and Fairness + participant Admission Control + participant REST Endpoint + + Client->>Handler Chain: Request + Handler Chain->>Authentication: Authenticate + Authentication-->>Handler Chain: User Info + Handler Chain->>Authorization: Authorize + Authorization-->>Handler Chain: Allowed/Denied + Handler Chain->>Priority and Fairness: Classify & Queue + Priority and Fairness-->>Handler Chain: Proceed + Handler Chain->>Admission Control: Mutate & Validate + Admission Control-->>Handler Chain: Object OK + Handler Chain->>REST Endpoint: Handle + REST Endpoint-->>Handler Chain: Response + Handler Chain-->>Client: Response +``` + +The handler chain consists of the following stages: + +1. **Authentication (`pkg/authentication`):** This filter identifies the user. The system is + pluggable and composed of multiple authenticators (e.g., client certs, bearer tokens, OIDC). + The identity of the user is determined by the first authenticator in the chain that successfully + identifies the user. +2. **Authorization (`pkg/authorization`):** This filter checks if the user is permitted to + perform the action. This system is also pluggable and composed of multiple authorizers + (e.g., RBAC, Node, Webhook). Each authorizer may respond with either allow, deny, or no opinion. + If the response is no opinion, the request is passed to the next authorizer in the chain. +3. **Priority and Fairness (`pkg/util/flowcontrol`):** This subsystem manages request + concurrency, classifying requests into `FlowSchema`s and `PriorityLevel`s to prevent + overload. This feature was introduced to prevent high traffic from overwhelming the API + server and to ensure that critical cluster operations are not starved. +4. **Admission Control (`pkg/admission`):** This is the primary mechanism for policy + enforcement. It is only at this stage that the request body is deserialized into an + object. It is a chain of plugins that can mutate or validate an object. The built-in Pod + Security admission controller is a key example of this, enforcing Pod Security Standards + at the namespace level. +5. **REST Endpoint Handling (`pkg/endpoints`):** The request is finally dispatched to the + appropriate REST handler, which is installed by the `APIInstaller`. + +## 3. API Group Registration + +The high-level steps for introducing an API are: + +1. **Define Types:** Create or modify the Go structs in the `types.go` file for the API group. +2. **Generate Code:** Use the code generators provided by the Kubernetes project to create the required + boilerplate methods for deep-copy, conversion, and defaulting. +3. **Implement the `Strategy`:** Write the custom business logic and validation for the + resource in its `Strategy` object. +4. **Register and Install:** Create the `APIGroupInfo` struct, bundling the `Scheme` and the + `Strategy`-configured storage, and pass it to the `GenericAPIServer`'s `InstallAPIGroup` + method. + +### The API Group Registry + +The `runtime.Scheme` acts as a central registry for an API group's type information. A single +`Scheme` object is created for each API group and is responsible for the following key +capabilities: + +* **Type Registration and Mapping:** The `Scheme`'s primary role is to map a GroupVersionKind + (GVK) to its corresponding Go type and back. This process also relies on the `deepcopy-gen` + tool to create `DeepCopy()` methods for each type, which is critical for ensuring that + objects returned from caches are never modified directly. + +* **API Conversion:** The `Scheme` stores the conversion functions that translate objects + between different API versions. These functions are typically generated by the + `conversion-gen` tool and enable the **hub-and-spoke** model. + +* **Defaulting:** The `Scheme` registers defaulting functions that populate optional fields in + an object. These are usually generated by the `defaulter-gen` tool. + +* **Declarative Validation:** The `Scheme` can store and execute code-generated validation + functions, providing a baseline level of validation. This is distinct from the primary, + handwritten business logic validation, which is handled by the `Strategy` object. + +### The `APIGroupInfo` Struct and `Strategy` Object + +With a populated `Scheme`, the API group is registered with the `GenericAPIServer` by bundling +the `Scheme` with the storage backend and versioning information into an `APIGroupInfo` struct. + +```mermaid +graph TD + subgraph Server Configuration + A[APIGroupInfo for apps v1]; + A --> B{Scheme: Knows Deployment v1}; + A --> C{Storage: deployments RESTStorage}; + A --> D{Version Priority: v1, v1beta1}; + end + + subgraph RESTStorage Implementation + C --> E[genericregistry.Store]; + E --> F[etcd client]; + E --> G[Deployment Strategy]; + end + + subgraph Server Runtime + H[GenericAPIServer] -- InstallAPIGroup --> I[APIInstaller]; + I -- Uses --> A; + I --> J{Register /apis/apps/v1/deployments}; + J --> K[HTTP Handler]; + K -- On Request --> C; + end +``` + +The registration process follows these steps: + +1. **`APIGroupInfo` Construction:** For each API group, an `APIGroupInfo` struct is created, + which contains the populated `Scheme`, a map of resources to their storage + implementations, and an ordered list of **Version Priority**. + +2. **REST Storage Instantiation:** For each resource, a `genericregistry.Store` is created. It + is configured with a resource-specific `Strategy` object that contains the core business + logic (e.g., handwritten validation). + +3. **API Group Installation:** The `GenericAPIServer`'s `InstallAPIGroup` method takes the + `APIGroupInfo` and uses an `APIInstaller` to expose the resources as HTTP endpoints. + +## 4. Watch Cache + +To handle the high volume of watch requests from controllers without overwhelming etcd, the +apiserver uses a **watch cache**. The implementation can be found in +`staging/src/k8s.io/apiserver/pkg/storage/cacher/`. + +* **Initialization:** The cacher first performs a `LIST` to get the current state of all + objects and a `ResourceVersion` for that point-in-time. It then starts a `WATCH` from + that version to ensure a consistent stream of events. +* **Serving from Cache:** Most list and watch requests are served from this in-memory cache, which + dramatically reduces the load on etcd. Consistent reads are also served from the + cache. This is achieved by first fetching the revision number of the latest write from + etcd. The server then ensures the cache is at least that recent—waiting for it to + refresh if necessary—before serving the request. +* **Fallback to Storage:** If a client request cannot be served from the + cache's buffer, the request "falls through" to the underlying etcd storage. +* **Bookmarks:** The cacher uses bookmark events to track the latest `ResourceVersion` for + unchanged objects. This prevents the cache's `ResourceVersion` from becoming too old, + which avoids the need for expensive relist operations from etcd when the objects have + not been modified. + +## 5. Conflict Resolution + +* **Optimistic Concurrency via `resourceVersion`:** Clients are expected to perform updates using a + read-modify-write workflow. The apiserver uses the `resourceVersion` field of every + object to enforce optimistic concurrency. This `resourceVersion` is not an arbitrary number; + it maps directly to etcd's globally consistent `mod_revision`. When a client submits an + update (`PUT` or `PATCH`), it must provide the `resourceVersion` of the object it based its + modifications on. If the `resourceVersion` on the server does not match the current + `mod_revision` in etcd, the server rejects the request with a `409 Conflict` error. This + forces the client to re-read the object, resolve the conflict, and resubmit with the new + `resourceVersion`. +* **Server-Side Apply:** A declarative, "intent-based" patch. The server maintains a + `managedFields` section in the object's metadata to track which "manager" (e.g., a + controller) owns each field. This allows multiple actors to manage different parts of the + same object without overwriting each other's changes. + +## 6. Discovery and OpenAPI + +Apiservers serve the `/apis` discovery endpoints and the `/openapi/v2` and `/openapi/v3` +specifications. The generation of the OpenAPI specification is a multi-stage process. + +* **`openapi-gen`**: This tool reflects on Go structs, reads godoc comments, and looks at + validation struct tags to generate a map of all API definitions. +* **`zz_generated.openapi.go`**: The output is a large Go file containing a + `GetOpenAPIDefinitions` function. +* **Runtime**: The `GenericAPIServer` calls this generated function to build the final OpenAPI + JSON spec that it serves to clients. + +## 7. Security & Observability + +* **Audit (`pkg/audit`):** The apiserver has a policy-driven event logging pipeline. The audit + policy controls what is logged and at which stage of a request. +* **Security:** + * **mTLS:** The primary authentication mechanism for system components. + * **Service Account Token Issuance:** The `kube-apiserver` acts as an OIDC provider, + issuing and validating JWTs for `ServiceAccount`s. + +## 8. Streaming Protocols + +* **Websockets:** The apiserver uses websockets to upgrade HTTP + connections for interactive, streaming protocols like `exec`, `attach`, and + `port-forward`. The `UpgradeAwareProxyHandler` manages this process. diff --git a/third_party/k8s.io/apiserver-v0.34.1/CONTRIBUTING.md b/third_party/k8s.io/apiserver-v0.36.1/CONTRIBUTING.md similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/CONTRIBUTING.md rename to third_party/k8s.io/apiserver-v0.36.1/CONTRIBUTING.md diff --git a/third_party/k8s.io/apiserver-v0.34.1/LICENSE b/third_party/k8s.io/apiserver-v0.36.1/LICENSE similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/LICENSE rename to third_party/k8s.io/apiserver-v0.36.1/LICENSE diff --git a/third_party/k8s.io/apiserver-v0.34.1/README.md b/third_party/k8s.io/apiserver-v0.36.1/README.md similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/README.md rename to third_party/k8s.io/apiserver-v0.36.1/README.md diff --git a/third_party/k8s.io/apiserver-v0.34.1/SECURITY_CONTACTS b/third_party/k8s.io/apiserver-v0.36.1/SECURITY_CONTACTS similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/SECURITY_CONTACTS rename to third_party/k8s.io/apiserver-v0.36.1/SECURITY_CONTACTS diff --git a/third_party/k8s.io/apiserver-v0.34.1/code-of-conduct.md b/third_party/k8s.io/apiserver-v0.36.1/code-of-conduct.md similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/code-of-conduct.md rename to third_party/k8s.io/apiserver-v0.36.1/code-of-conduct.md diff --git a/third_party/k8s.io/apiserver-v0.36.1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/doc.go new file mode 100644 index 000000000..78b4becd7 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/doc.go @@ -0,0 +1,74 @@ +/* +Copyright 2021 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 apiserver provides the machinery for building Kubernetes-style API servers. +// +// This library is the foundation for the Kubernetes API server (`kube-apiserver`), +// and is also the primary framework for developers building custom API servers to extend +// the Kubernetes API. +// +// An extension API server is a user-provided, standalone web server that registers itself +// with the main kube-apiserver to handle specific API groups. This allows developers to +// extend Kubernetes with their own APIs that behave like core Kubernetes APIs, complete +// with typed clients, authentication, authorization, and discovery. +// +// # Key Packages +// +// The `apiserver` library is composed of several key packages: +// +// - `pkg/server`: This is the core of the library, providing the `GenericAPIServer` +// and the main machinery for building the server. +// - `pkg/admission`: This package contains the admission control framework. Developers +// can use this to build custom admission plugins that can validate or mutate +// requests to enforce custom policies. This is a common way to extend Kubernetes +// behavior without adding a full API server. +// - `pkg/authentication`: This package provides the framework for authenticating +// requests. +// - `pkg/authorization`: This package provides the framework for authorizing +// requests. +// - `pkg/endpoints`: This package contains the machinery for building the REST +// endpoints for the API server. +// - `pkg/registry`: This package provides the storage interface for the API server. +// +// # Instantiating a GenericAPIServer +// +// The `GenericAPIServer` struct is the heart of any extension server. It is responsible +// for assembling and running the HTTP serving stack. See the runnable example for a +// demonstration of how to instantiate a `GenericAPIServer`. +// +// # Building an Extension API Server (API Aggregation) +// +// The mechanism that enables extension API servers is API aggregation. The +// primary apiserver (typically the kube-apiserver) acts as a proxy, forwarding +// requests for a specific API group (e.g., /apis/myextension.io/v1) to a +// registered extension server. The apiserver is configured using +// APIService objects. +// +// For most use cases, custom resources (CustomResourceDefinitions) are the +// preferred way to extend the Kubernetes API. +// +// # Building an Admission Plugin +// +// The `pkg/admission` package provides a way to add admission policies directly +// into an apiserver. Admission plugins can be used to validate or mutate objects +// during write operations. The kube-apiserver uses admission plugins to provide +// a variety of core system capabilities. +// +// For most extension use cases dynamic admission control using policies +// (ValidatingAdmissionPolicies or MutatingAdmissionPolicies) or +// webhooks (ValidatingWebhookConfiguration and MutatingWebhookConfiguration) are the +// preferred way to extend admission control. +package apiserver diff --git a/third_party/k8s.io/apiserver-v0.36.1/go.mod b/third_party/k8s.io/apiserver-v0.36.1/go.mod new file mode 100644 index 000000000..474b1e215 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/go.mod @@ -0,0 +1,125 @@ +// This is a generated file. Do not edit directly. + +module k8s.io/apiserver + +go 1.26.0 + +godebug default=go1.26 + +require ( + github.com/blang/semver/v4 v4.0.0 + github.com/coreos/go-oidc v2.5.0+incompatible + github.com/coreos/go-systemd/v22 v22.7.0 + github.com/emicklei/go-restful/v3 v3.13.0 + github.com/fsnotify/fsnotify v1.9.0 + github.com/go-logr/logr v1.4.3 + github.com/go-openapi/jsonreference v0.20.2 + github.com/google/cel-go v0.26.0 + github.com/google/gnostic-models v0.7.0 + github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f + github.com/spf13/pflag v1.0.9 + github.com/stretchr/testify v1.11.1 + go.etcd.io/etcd/api/v3 v3.6.8 + go.etcd.io/etcd/client/pkg/v3 v3.6.8 + go.etcd.io/etcd/client/v3 v3.6.8 + go.etcd.io/etcd/server/v3 v3.6.8 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 + go.opentelemetry.io/otel/metric v1.41.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.41.0 + go.uber.org/zap v1.27.1 + golang.org/x/crypto v0.47.0 + golang.org/x/net v0.49.0 + golang.org/x/sync v0.19.0 + golang.org/x/sys v0.40.0 + golang.org/x/text v0.33.0 + golang.org/x/time v0.14.0 + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af + gopkg.in/evanphx/json-patch.v4 v4.13.0 + gopkg.in/go-jose/go-jose.v2 v2.6.3 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 + k8s.io/component-base v0.36.1 + k8s.io/klog/v2 v2.140.0 + k8s.io/kms v0.36.1 + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a + k8s.io/streaming v0.36.1 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 + sigs.k8s.io/randfill v1.0.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 + sigs.k8s.io/yaml v1.6.0 +) + +require ( + cel.dev/expr v0.25.1 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.5.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pquerna/cachecontrol v0.1.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect + go.etcd.io/bbolt v1.4.3 // indirect + go.etcd.io/etcd/pkg/v3 v3.6.8 // indirect + go.etcd.io/raft/v3 v3.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/term v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/third_party/k8s.io/apiserver-v0.34.1/go.sum b/third_party/k8s.io/apiserver-v0.36.1/go.sum similarity index 59% rename from third_party/k8s.io/apiserver-v0.34.1/go.sum rename to third_party/k8s.io/apiserver-v0.36.1/go.sum index 459898853..8144dc1fe 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/go.sum +++ b/third_party/k8s.io/apiserver-v0.36.1/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= @@ -10,27 +10,28 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= -github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc v2.5.0+incompatible h1:6W0vGJR3Tu0r0PwfmjOrRZSlfxeEln8dsejt3ZWIvwo= +github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -38,8 +39,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -52,13 +53,10 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -70,21 +68,17 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= @@ -108,8 +102,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -120,36 +114,31 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -164,8 +153,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -174,59 +163,59 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= -go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= -go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= -go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= -go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= -go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= -go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= -go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= -go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA= -go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE= -go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= -go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM= +go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q= +go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50= +go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw= +go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY= +go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8= +go.etcd.io/etcd/pkg/v3 v3.6.8 h1:Xe+LIL974spy8b4nEx3H0KMr1ofq3r0kh6FbU3aw4es= +go.etcd.io/etcd/pkg/v3 v3.6.8/go.mod h1:TRibVNe+FqJIe1abOAA1PsuQ4wqO87ZaOoprg09Tn8c= +go.etcd.io/etcd/server/v3 v3.6.8 h1:U2strdSEy1U8qcSzRIdkYpvOPtBy/9i/IfaaCI9flZ4= +go.etcd.io/etcd/server/v3 v3.6.8/go.mod h1:88dCtwUnSirkUoJbflQxxWXqtBSZa6lSG0Kuej+dois= go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= +golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -235,15 +224,15 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -252,41 +241,41 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= @@ -296,29 +285,31 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= -k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= -k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= -k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= -k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w= -k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA= +k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kms v0.36.1 h1:XdvKpywoW4k7YUHDh5uYP4mahJXECswHGfCddBBYLZs= +k8s.io/kms v0.36.1/go.mod h1:g91diTD9h0oJCCHkTb00krlF+Qm5HTnkWLi9Q/TpRoc= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4= +k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/attributes.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/attributes.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/attributes.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/attributes.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/attributes_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/attributes_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/attributes_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/attributes_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/audit.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/audit.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/audit.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/audit.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/audit_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/audit_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/audit_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/audit_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/chain.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/chain.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/chain.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/chain.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/chain_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/chain_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/chain_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/chain_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/config.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/config.go index 4bb5f27bf..1e64af447 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/config.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/config.go @@ -108,7 +108,7 @@ func ReadAdmissionConfiguration(pluginNames []string, configFilePath string, con // in order to preserve backwards compatibility, we set plugins that // previously read input from a non-versioned file configuration to the // current input file. - legacyPluginsWithUnversionedConfig := sets.NewString("ImagePolicyWebhook", "PodNodeSelector") + legacyPluginsWithUnversionedConfig := sets.New[string]("ImagePolicyWebhook", "PodNodeSelector") externalConfig := &apiserverv1.AdmissionConfiguration{} for _, pluginName := range pluginNames { if legacyPluginsWithUnversionedConfig.Has(pluginName) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/config_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/config_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/configuration_manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/configuration_manager.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/configuration_manager.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/configuration_manager.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/configuration_manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/configuration_manager_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/configuration_manager_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/configuration_manager_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/mutating_webhook_manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/mutating_webhook_manager.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/mutating_webhook_manager.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/mutating_webhook_manager.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/mutating_webhook_manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/mutating_webhook_manager_test.go similarity index 68% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/mutating_webhook_manager_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/mutating_webhook_manager_test.go index 801c4ecaa..383d0084c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/mutating_webhook_manager_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/mutating_webhook_manager_test.go @@ -20,16 +20,17 @@ import ( "context" "reflect" "testing" - "time" + "testing/synctest" - "k8s.io/api/admissionregistration/v1" + v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" ) -func TestGetMutatingWebhookConfig(t *testing.T) { +func TestGetMutatingWebhookConfig(t *testing.T) { synctest.Test(t, testGetMutatingWebhookConfig) } +func testGetMutatingWebhookConfig(t *testing.T) { // Build a test client that the admission plugin can use to look up the MutatingWebhookConfiguration client := fake.NewSimpleClientset() informerFactory := informers.NewSharedInformerFactory(client, 0) @@ -40,6 +41,9 @@ func TestGetMutatingWebhookConfig(t *testing.T) { informerFactory.Start(stop) informerFactory.WaitForCacheSync(stop) + // Wait for background activity to settle. + synctest.Wait() + // no configurations if configurations := manager.Webhooks(); len(configurations) != 0 { t.Errorf("expected empty webhooks, but got %v", configurations) @@ -55,19 +59,10 @@ func TestGetMutatingWebhookConfig(t *testing.T) { MutatingWebhookConfigurations(). Create(context.TODO(), webhookConfiguration, metav1.CreateOptions{}) - // Wait up to 10s for the notification to be delivered. - // (on my system this takes < 2ms) - startTime := time.Now() + // Wait for the notification to be delivered. + synctest.Wait() configurations := manager.Webhooks() - for len(configurations) == 0 { - if time.Since(startTime) > 10*time.Second { - break - } - time.Sleep(time.Millisecond) - configurations = manager.Webhooks() - } - // verify presence if len(configurations) == 0 { t.Errorf("expected non empty webhooks") } @@ -212,72 +207,72 @@ func TestGetMutatingWebhookConfigSmartReload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := fake.NewSimpleClientset() - informerFactory := informers.NewSharedInformerFactory(client, 0) - stop := make(chan struct{}) - defer close(stop) - manager := NewMutatingWebhookConfigurationManager(informerFactory) - managerStructPtr := manager.(*mutatingWebhookConfigurationManager) - fakeWebhookAccessorCreator := &mockCreateMutatingWebhookAccessor{} - managerStructPtr.createMutatingWebhookAccessor = fakeWebhookAccessorCreator.fn - informerFactory.Start(stop) - informerFactory.WaitForCacheSync(stop) + synctest.Test(t, func(t *testing.T) { + client := fake.NewSimpleClientset() + informerFactory := informers.NewSharedInformerFactory(client, 0) + stop := make(chan struct{}) + defer close(stop) + manager := NewMutatingWebhookConfigurationManager(informerFactory) + managerStructPtr := manager.(*mutatingWebhookConfigurationManager) + fakeWebhookAccessorCreator := &mockCreateMutatingWebhookAccessor{} + managerStructPtr.createMutatingWebhookAccessor = fakeWebhookAccessorCreator.fn + informerFactory.Start(stop) + informerFactory.WaitForCacheSync(stop) + + // Create webhooks + for _, configurations := range tt.args.createWebhookConfigurations { + client. + AdmissionregistrationV1(). + MutatingWebhookConfigurations(). + Create(context.TODO(), configurations, metav1.CreateOptions{}) + } + + synctest.Wait() + webhooks := manager.Webhooks() + if mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) { + t.Errorf("Expected number of webhooks %d received %d", + mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations), + len(webhooks), + ) + } + // assert creations + if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() { + t.Errorf( + "Expected number of creations %d received %d", + tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(), + ) + } - // Create webhooks - for _, configurations := range tt.args.createWebhookConfigurations { - client. - AdmissionregistrationV1(). - MutatingWebhookConfigurations(). - Create(context.TODO(), configurations, metav1.CreateOptions{}) - } - // TODO use channels to wait for manager.createMutatingWebhookAccessor - // to be called instead of using time.Sleep - time.Sleep(1 * time.Second) - webhooks := manager.Webhooks() - if mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) { - t.Errorf("Expected number of webhooks %d received %d", - mutatingConfigurationTotalWebhooks(tt.args.createWebhookConfigurations), - len(webhooks), - ) - } - // assert creations - if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() { - t.Errorf( - "Expected number of creations %d received %d", - tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(), - ) - } + // reset mock counter + fakeWebhookAccessorCreator.resetCounter() - // reset mock counter - fakeWebhookAccessorCreator.resetCounter() + // Update webhooks + for _, configurations := range tt.args.updateWebhookConfigurations { + client. + AdmissionregistrationV1(). + MutatingWebhookConfigurations(). + Update(context.TODO(), configurations, metav1.UpdateOptions{}) + } - // Update webhooks - for _, configurations := range tt.args.updateWebhookConfigurations { - client. - AdmissionregistrationV1(). - MutatingWebhookConfigurations(). - Update(context.TODO(), configurations, metav1.UpdateOptions{}) - } - // TODO use channels to wait for manager.createMutatingWebhookAccessor - // to be called instead of using time.Sleep - time.Sleep(1 * time.Second) - webhooks = manager.Webhooks() - if tt.finalNumberOfWebhookAccessors != len(webhooks) { - t.Errorf("Expected final number of webhooks %d received %d", - tt.finalNumberOfWebhookAccessors, - len(webhooks), - ) - } + synctest.Wait() + webhooks = manager.Webhooks() + if tt.finalNumberOfWebhookAccessors != len(webhooks) { + t.Errorf("Expected final number of webhooks %d received %d", + tt.finalNumberOfWebhookAccessors, + len(webhooks), + ) + } - // assert updates - if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() { - t.Errorf( - "Expected number of refreshes %d received %d", - tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(), - ) - } - // reset mock counter for the next test cases - fakeWebhookAccessorCreator.resetCounter() + // assert updates + if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() { + t.Errorf( + "Expected number of refreshes %d received %d", + tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(), + ) + } + // reset mock counter for the next test cases + fakeWebhookAccessorCreator.resetCounter() + }) }) } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/validating_webhook_manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/validating_webhook_manager.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/validating_webhook_manager.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/validating_webhook_manager.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/validating_webhook_manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/validating_webhook_manager_test.go similarity index 68% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/validating_webhook_manager_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/validating_webhook_manager_test.go index 2f18f386b..9446f11b1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/configuration/validating_webhook_manager_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/configuration/validating_webhook_manager_test.go @@ -20,16 +20,17 @@ import ( "context" "reflect" "testing" - "time" + "testing/synctest" - "k8s.io/api/admissionregistration/v1" + v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/admission/plugin/webhook" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" ) -func TestGetValidatingWebhookConfig(t *testing.T) { +func TestGetValidatingWebhookConfig(t *testing.T) { synctest.Test(t, testGetValidatingWebhookConfig) } +func testGetValidatingWebhookConfig(t *testing.T) { // Build a test client that the admission plugin can use to look up the ValidatingWebhookConfiguration client := fake.NewSimpleClientset() informerFactory := informers.NewSharedInformerFactory(client, 0) @@ -41,6 +42,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) { informerFactory.WaitForCacheSync(stop) // no configurations + synctest.Wait() if configurations := manager.Webhooks(); len(configurations) != 0 { t.Errorf("expected empty webhooks, but got %v", configurations) } @@ -55,17 +57,9 @@ func TestGetValidatingWebhookConfig(t *testing.T) { ValidatingWebhookConfigurations(). Create(context.TODO(), webhookConfiguration, metav1.CreateOptions{}) - // Wait up to 10s for the notification to be delivered. - // (on my system this takes < 2ms) - startTime := time.Now() + // Wait for the notification to be delivered. + synctest.Wait() configurations := manager.Webhooks() - for len(configurations) == 0 { - if time.Since(startTime) > 10*time.Second { - break - } - time.Sleep(time.Millisecond) - configurations = manager.Webhooks() - } // verify presence if len(configurations) == 0 { @@ -212,72 +206,71 @@ func TestGetValidatingWebhookConfigSmartReload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := fake.NewSimpleClientset() - informerFactory := informers.NewSharedInformerFactory(client, 0) - stop := make(chan struct{}) - defer close(stop) - manager := NewValidatingWebhookConfigurationManager(informerFactory) - managerStructPtr := manager.(*validatingWebhookConfigurationManager) - fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{} - managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn - informerFactory.Start(stop) - informerFactory.WaitForCacheSync(stop) + synctest.Test(t, func(t *testing.T) { + client := fake.NewSimpleClientset() + informerFactory := informers.NewSharedInformerFactory(client, 0) + stop := make(chan struct{}) + defer close(stop) + manager := NewValidatingWebhookConfigurationManager(informerFactory) + managerStructPtr := manager.(*validatingWebhookConfigurationManager) + fakeWebhookAccessorCreator := &mockCreateValidatingWebhookAccessor{} + managerStructPtr.createValidatingWebhookAccessor = fakeWebhookAccessorCreator.fn + informerFactory.Start(stop) + informerFactory.WaitForCacheSync(stop) + + // Create webhooks + for _, configurations := range tt.args.createWebhookConfigurations { + client. + AdmissionregistrationV1(). + ValidatingWebhookConfigurations(). + Create(context.TODO(), configurations, metav1.CreateOptions{}) + } - // Create webhooks - for _, configurations := range tt.args.createWebhookConfigurations { - client. - AdmissionregistrationV1(). - ValidatingWebhookConfigurations(). - Create(context.TODO(), configurations, metav1.CreateOptions{}) - } - // TODO use channels to wait for manager.createValidatingWebhookAccessor - // to be called instead of using time.Sleep - time.Sleep(1 * time.Second) - webhooks := manager.Webhooks() - if configurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) { - t.Errorf("Expected number of webhooks %d received %d", - configurationTotalWebhooks(tt.args.createWebhookConfigurations), - len(webhooks), - ) - } - // assert creations - if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() { - t.Errorf( - "Expected number of creations %d received %d", - tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(), - ) - } + synctest.Wait() + webhooks := manager.Webhooks() + if configurationTotalWebhooks(tt.args.createWebhookConfigurations) != len(webhooks) { + t.Errorf("Expected number of webhooks %d received %d", + configurationTotalWebhooks(tt.args.createWebhookConfigurations), + len(webhooks), + ) + } + // assert creations + if tt.numberOfCreations != fakeWebhookAccessorCreator.calledNTimes() { + t.Errorf( + "Expected number of creations %d received %d", + tt.numberOfCreations, fakeWebhookAccessorCreator.calledNTimes(), + ) + } - // reset mock counter - fakeWebhookAccessorCreator.resetCounter() + // reset mock counter + fakeWebhookAccessorCreator.resetCounter() - // Update webhooks - for _, configurations := range tt.args.updateWebhookConfigurations { - client. - AdmissionregistrationV1(). - ValidatingWebhookConfigurations(). - Update(context.TODO(), configurations, metav1.UpdateOptions{}) - } - // TODO use channels to wait for manager.createValidatingWebhookAccessor - // to be called instead of using time.Sleep - time.Sleep(1 * time.Second) - webhooks = manager.Webhooks() - if tt.finalNumberOfWebhookAccessors != len(webhooks) { - t.Errorf("Expected final number of webhooks %d received %d", - tt.finalNumberOfWebhookAccessors, - len(webhooks), - ) - } + // Update webhooks + for _, configurations := range tt.args.updateWebhookConfigurations { + client. + AdmissionregistrationV1(). + ValidatingWebhookConfigurations(). + Update(context.TODO(), configurations, metav1.UpdateOptions{}) + } + synctest.Wait() + webhooks = manager.Webhooks() + if tt.finalNumberOfWebhookAccessors != len(webhooks) { + t.Errorf("Expected final number of webhooks %d received %d", + tt.finalNumberOfWebhookAccessors, + len(webhooks), + ) + } - // assert updates - if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() { - t.Errorf( - "Expected number of refreshes %d received %d", - tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(), - ) - } - // reset mock counter for the next test cases - fakeWebhookAccessorCreator.resetCounter() + // assert updates + if tt.numberOfRefreshes != fakeWebhookAccessorCreator.calledNTimes() { + t.Errorf( + "Expected number of refreshes %d received %d", + tt.numberOfRefreshes, fakeWebhookAccessorCreator.calledNTimes(), + ) + } + // reset mock counter for the next test cases + fakeWebhookAccessorCreator.resetCounter() + }) }) } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/conversion_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/conversion_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/conversion_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/conversion_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/decorator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/decorator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/decorator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/decorator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/errors_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/errors_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/errors_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/errors_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/handler.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/handler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/handler.go index d2a9e7d4c..e498b78f6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/handler.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/handler.go @@ -34,7 +34,7 @@ type ReadyFunc func() bool // Handler is a base for admission control handlers that // support a predefined set of operations type Handler struct { - operations sets.String + operations sets.Set[string] readyFunc ReadyFunc } @@ -46,7 +46,7 @@ func (h *Handler) Handles(operation Operation) bool { // NewHandler creates a new base handler that handles the passed // in operations func NewHandler(ops ...Operation) *Handler { - operations := sets.NewString() + operations := sets.New[string]() for _, op := range ops { operations.Insert(string(op)) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/handler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/handler_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/handler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/handler_test.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/apiserver_id.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/apiserver_id.go new file mode 100644 index 000000000..b708d03ad --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/apiserver_id.go @@ -0,0 +1,43 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package initializer + +import "k8s.io/apiserver/pkg/admission" + +// apiServerIDInitializer injects the API server identity into admission plugins +// that implement WantsAPIServerID. This is used for metrics labeling in HA setups +// where multiple API servers may be running with different manifest configurations. +type apiServerIDInitializer struct { + apiServerID string +} + +var _ admission.PluginInitializer = &apiServerIDInitializer{} + +// NewAPIServerIDInitializer returns a PluginInitializer that injects the given +// API server ID into admission plugins that implement WantsAPIServerID. +// This initializer must be placed before the generic initializer in the chain +// so that the API server ID is available when SetExternalKubeInformerFactory is called. +func NewAPIServerIDInitializer(apiServerID string) admission.PluginInitializer { + return &apiServerIDInitializer{apiServerID: apiServerID} +} + +// Initialize checks whether the plugin implements WantsAPIServerID and injects the ID. +func (i *apiServerIDInitializer) Initialize(plugin admission.Interface) { + if wants, ok := plugin.(WantsAPIServerID); ok { + wants.SetAPIServerID(i.apiServerID) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer.go index 21ee8c801..b78802968 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer.go @@ -23,6 +23,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" + "k8s.io/component-base/compatibility" "k8s.io/component-base/featuregate" ) @@ -32,6 +33,7 @@ type pluginInitializer struct { externalInformers informers.SharedInformerFactory authorizer authorizer.Authorizer featureGates featuregate.FeatureGate + effectiveVersion compatibility.EffectiveVersion stopCh <-chan struct{} restMapper meta.RESTMapper } @@ -45,6 +47,7 @@ func New( extInformers informers.SharedInformerFactory, authz authorizer.Authorizer, featureGates featuregate.FeatureGate, + effectiveVersion compatibility.EffectiveVersion, stopCh <-chan struct{}, restMapper meta.RESTMapper, ) pluginInitializer { @@ -54,6 +57,7 @@ func New( externalInformers: extInformers, authorizer: authz, featureGates: featureGates, + effectiveVersion: effectiveVersion, stopCh: stopCh, restMapper: restMapper, } @@ -68,6 +72,9 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) { } // Second tell the plugin about enabled features, so it can decide whether to start informers or not + if wants, ok := plugin.(WantsEffectiveVersion); ok { + wants.InspectEffectiveVersion(i.effectiveVersion) + } if wants, ok := plugin.(WantsFeatures); ok { wants.InspectFeatureGates(i.featureGates) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer_test.go index d80ce6dd4..d503001d6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/initializer_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/initializer_test.go @@ -34,7 +34,7 @@ import ( // TestWantsAuthorizer ensures that the authorizer is injected // when the WantsAuthorizer interface is implemented by a plugin. func TestWantsAuthorizer(t *testing.T) { - target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil, nil) + target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil, nil, nil) wantAuthorizerAdmission := &WantAuthorizerAdmission{} target.Initialize(wantAuthorizerAdmission) if wantAuthorizerAdmission.auth == nil { @@ -46,7 +46,7 @@ func TestWantsAuthorizer(t *testing.T) { // when the WantsExternalKubeClientSet interface is implemented by a plugin. func TestWantsExternalKubeClientSet(t *testing.T) { cs := &fake.Clientset{} - target := initializer.New(cs, nil, nil, &TestAuthorizer{}, nil, nil, nil) + target := initializer.New(cs, nil, nil, &TestAuthorizer{}, nil, nil, nil, nil) wantExternalKubeClientSet := &WantExternalKubeClientSet{} target.Initialize(wantExternalKubeClientSet) if wantExternalKubeClientSet.cs != cs { @@ -59,7 +59,7 @@ func TestWantsExternalKubeClientSet(t *testing.T) { func TestWantsExternalKubeInformerFactory(t *testing.T) { cs := &fake.Clientset{} sf := informers.NewSharedInformerFactory(cs, time.Duration(1)*time.Second) - target := initializer.New(cs, nil, sf, &TestAuthorizer{}, nil, nil, nil) + target := initializer.New(cs, nil, sf, &TestAuthorizer{}, nil, nil, nil, nil) wantExternalKubeInformerFactory := &WantExternalKubeInformerFactory{} target.Initialize(wantExternalKubeInformerFactory) if wantExternalKubeInformerFactory.sf != sf { @@ -71,7 +71,7 @@ func TestWantsExternalKubeInformerFactory(t *testing.T) { // when the WantsShutdownSignal interface is implemented by a plugin. func TestWantsShutdownNotification(t *testing.T) { stopCh := make(chan struct{}) - target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, stopCh, nil) + target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil, stopCh, nil) wantDrainedNotification := &WantDrainedNotification{} target.Initialize(wantDrainedNotification) if wantDrainedNotification.stopCh == nil { @@ -153,7 +153,7 @@ func (t *TestAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) } func TestRESTMapperAdmissionPlugin(t *testing.T) { - initializer := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil, &doNothingRESTMapper{}) + initializer := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil, nil, &doNothingRESTMapper{}) wantsRESTMapperAdmission := &WantsRESTMapperAdmissionPlugin{} initializer.Initialize(wantsRESTMapperAdmission) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/interfaces.go similarity index 58% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/interfaces.go index 21202bd79..cd3da1a61 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/initializer/interfaces.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/initializer/interfaces.go @@ -17,6 +17,7 @@ limitations under the License. package initializer import ( + admissionregistrationv1api "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" @@ -26,6 +27,7 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" + "k8s.io/component-base/compatibility" "k8s.io/component-base/featuregate" ) @@ -73,6 +75,12 @@ type WantsFeatures interface { admission.InitializationValidator } +// WantsEffectiveVersion defines a function which passes the effective version for inspection by an admission plugin. +type WantsEffectiveVersion interface { + InspectEffectiveVersion(compatibility.EffectiveVersion) + admission.InitializationValidator +} + type WantsDynamicClient interface { SetDynamicClient(dynamic.Interface) admission.InitializationValidator @@ -97,3 +105,41 @@ type WantsExcludedAdmissionResources interface { SetExcludedAdmissionResources(excludedAdmissionResources []schema.GroupResource) admission.InitializationValidator } + +// WantsAPIServerID defines a function which sets the API server ID for admission plugins +// that need it (e.g., for metrics labeling in HA setups). +type WantsAPIServerID interface { + SetAPIServerID(apiServerID string) + admission.InitializationValidator +} + +// ValidatingWebhookManifestLoadFunc loads ValidatingWebhookConfiguration manifests from a directory. +type ValidatingWebhookManifestLoadFunc func(dir string) ([]*admissionregistrationv1api.ValidatingWebhookConfiguration, string, error) + +// MutatingWebhookManifestLoadFunc loads MutatingWebhookConfiguration manifests from a directory. +type MutatingWebhookManifestLoadFunc func(dir string) ([]*admissionregistrationv1api.MutatingWebhookConfiguration, string, error) + +// ValidatingPolicyManifestLoadFunc loads ValidatingAdmissionPolicy manifests from a directory. +type ValidatingPolicyManifestLoadFunc func(dir string) ([]*admissionregistrationv1api.ValidatingAdmissionPolicy, []*admissionregistrationv1api.ValidatingAdmissionPolicyBinding, string, error) + +// MutatingPolicyManifestLoadFunc loads MutatingAdmissionPolicy manifests from a directory. +type MutatingPolicyManifestLoadFunc func(dir string) ([]*admissionregistrationv1api.MutatingAdmissionPolicy, []*admissionregistrationv1api.MutatingAdmissionPolicyBinding, string, error) + +// ManifestLoaders provides functions to load admission configurations from static manifest files +// with scheme-based defaulting and validation. +type ManifestLoaders struct { + // LoadValidatingWebhookManifests loads ValidatingWebhookConfiguration manifests. + LoadValidatingWebhookManifests ValidatingWebhookManifestLoadFunc + // LoadMutatingWebhookManifests loads MutatingWebhookConfiguration manifests. + LoadMutatingWebhookManifests MutatingWebhookManifestLoadFunc + // LoadValidatingPolicyManifests loads ValidatingAdmissionPolicy manifests. + LoadValidatingPolicyManifests ValidatingPolicyManifestLoadFunc + // LoadMutatingPolicyManifests loads MutatingAdmissionPolicy manifests. + LoadMutatingPolicyManifests MutatingPolicyManifestLoadFunc +} + +// WantsManifestLoaders is implemented by admission plugins that load configurations +// from static manifest files and need scheme-based defaulting and validation. +type WantsManifestLoaders interface { + SetManifestLoaders(loaders *ManifestLoaders) +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/testutil_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/testutil_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/metrics/testutil_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/metrics/testutil_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/authorizer/caching_authorizer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/authorizer/caching_authorizer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/authorizer/caching_authorizer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/authorizer/caching_authorizer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/authorizer/caching_authorizer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/authorizer/caching_authorizer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/authorizer/caching_authorizer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/authorizer/caching_authorizer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/activation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/activation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/activation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/activation.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile.go index f0fff1304..fb5dd8252 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile.go @@ -229,18 +229,19 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { for _, hasParams := range []bool{false, true} { for _, hasAuthorizer := range []bool{false, true} { var err error - for _, strictCost := range []bool{false, true} { - decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost} + { + decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer} envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl) if err != nil { panic(err) } } - // We only need this ObjectTypes where strict cost is true - decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true} - envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl) - if err != nil { - panic(err) + { + decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, HasPatchTypes: true} + envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl) + if err != nil { + panic(err) + } } } } @@ -274,16 +275,11 @@ func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.D requestType, }, }, + environment.StrictCostOpt, ) if err != nil { return nil, fmt.Errorf("environment misconfigured: %w", err) } - if opts.StrictCost { - extended, err = extended.Extend(environment.StrictCostOpt) - if err != nil { - return nil, fmt.Errorf("environment misconfigured: %w", err) - } - } if opts.HasPatchTypes { extended, err = extended.Extend(hasPatchTypes) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile_test.go index 2059cc524..5caf82858 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/compile_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/compile_test.go @@ -178,7 +178,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { } // Include the test library, which includes the test() function in the storage environment during test - base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true) + base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) extended, err := base.Extend(environment.VersionedOptions{ IntroducedVersion: version.MajorMinor(1, 999), EnvOptions: []celgo.EnvOption{library.Test()}, @@ -254,7 +254,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { } func BenchmarkCompile(b *testing.B) { - compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) b.ResetTimer() for i := 0; i < b.N; i++ { options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition.go similarity index 73% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition.go index bf8715a14..d7456d090 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition.go @@ -34,7 +34,7 @@ import ( "k8s.io/apiserver/pkg/cel/lazy" ) -const VariablesTypeName = "kubernetes.variables" +const variablesTypeName = "kubernetes.variables" // CompositedCompiler compiles expressions with variable composition. type CompositedCompiler struct { @@ -42,7 +42,7 @@ type CompositedCompiler struct { ConditionCompiler MutatingCompiler - CompositionEnv *CompositionEnv + state *compositionState } // CompositedConditionEvaluator provides evaluation of a condition expression with variable composition. @@ -50,7 +50,7 @@ type CompositedCompiler struct { type CompositedConditionEvaluator struct { ConditionEvaluator - compositionEnv *CompositionEnv + state *compositionState } // CompositedEvaluator provides evaluation of a single expression with variable composition. @@ -58,32 +58,78 @@ type CompositedConditionEvaluator struct { type CompositedEvaluator struct { MutatingEvaluator - compositionEnv *CompositionEnv + state *compositionState } func NewCompositedCompiler(envSet *environment.EnvSet) (*CompositedCompiler, error) { - compositionContext, err := NewCompositionEnv(VariablesTypeName, envSet) + newMapType := apiservercel.NewObjectType(variablesTypeName, map[string]*apiservercel.DeclField{}) + + newEnvSet, err := envSet.Extend(environment.VersionedOptions{ + IntroducedVersion: version.MajorMinor(1, 0), + EnvOptions: []cel.EnvOption{ + cel.Variable("variables", newMapType.CelType()), + }, + DeclTypes: []*apiservercel.DeclType{ + newMapType, + }, + }) if err != nil { return nil, err } - return NewCompositedCompilerFromTemplate(compositionContext), nil -} -func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompiler { - context = &CompositionEnv{ - MapType: context.MapType, - EnvSet: context.EnvSet, - CompiledVariables: map[string]CompilationResult{}, + state := &compositionState{ + mapType: newMapType, + EnvSet: newEnvSet, + compiledVariables: map[string]CompilationResult{}, } - compiler := NewCompiler(context.EnvSet) + + compiler := NewCompiler(state.EnvSet) conditionCompiler := &conditionCompiler{compiler} mutation := &mutatingCompiler{compiler} return &CompositedCompiler{ Compiler: compiler, ConditionCompiler: conditionCompiler, MutatingCompiler: mutation, - CompositionEnv: context, + state: state, + }, nil +} + +// NewCompositedCompilerForTypeChecking creates a CompositedCompiler for type checking. +// It initializes the composition state but leaves the Compilers nil, as they are expected +// to be replaced by the caller (who is doing type checking). +func NewCompositedCompilerForTypeChecking(envSet *environment.EnvSet) (*CompositedCompiler, error) { + newMapType := apiservercel.NewObjectType(variablesTypeName, map[string]*apiservercel.DeclField{}) + + newEnvSet, err := envSet.Extend(environment.VersionedOptions{ + IntroducedVersion: version.MajorMinor(1, 0), + EnvOptions: []cel.EnvOption{ + cel.Variable("variables", newMapType.CelType()), + }, + DeclTypes: []*apiservercel.DeclType{ + newMapType, + }, + }) + if err != nil { + return nil, err } + + state := &compositionState{ + mapType: newMapType, + EnvSet: newEnvSet, + compiledVariables: map[string]CompilationResult{}, + } + return &CompositedCompiler{ + state: state, + }, nil +} + +// Env returns the CEL environment for the given mode. +func (c *CompositedCompiler) Env(mode environment.Type) (*cel.Env, error) { + return c.state.Env(mode) +} + +func (c *CompositedCompiler) CreateContext(parent context.Context) CompositionContext { + return c.state.CreateContext(parent) } func (c *CompositedCompiler) CompileAndStoreVariables(variables []NamedExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) { @@ -94,8 +140,8 @@ func (c *CompositedCompiler) CompileAndStoreVariables(variables []NamedExpressio func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) CompilationResult { result := c.Compiler.CompileCELExpression(variable, options, mode) - c.CompositionEnv.AddField(variable.GetName(), result.OutputType) - c.CompositionEnv.CompiledVariables[variable.GetName()] = result + c.state.AddField(variable.GetName(), result.OutputType) + c.state.compiledVariables[variable.GetName()] = result return result } @@ -103,7 +149,7 @@ func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType) return &CompositedConditionEvaluator{ ConditionEvaluator: condition, - compositionEnv: c.CompositionEnv, + state: c.state, } } @@ -112,47 +158,25 @@ func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAcces mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType) return &CompositedEvaluator{ MutatingEvaluator: mutation, - compositionEnv: c.CompositionEnv, + state: c.state, } } -type CompositionEnv struct { +type compositionState struct { *environment.EnvSet - MapType *apiservercel.DeclType - CompiledVariables map[string]CompilationResult -} - -func (c *CompositionEnv) AddField(name string, celType *cel.Type) { - c.MapType.Fields[name] = apiservercel.NewDeclField(name, convertCelTypeToDeclType(celType), true, nil, nil) + mapType *apiservercel.DeclType + compiledVariables map[string]CompilationResult } -func NewCompositionEnv(typeName string, baseEnvSet *environment.EnvSet) (*CompositionEnv, error) { - declType := apiservercel.NewObjectType(typeName, map[string]*apiservercel.DeclField{}) - envSet, err := baseEnvSet.Extend(environment.VersionedOptions{ - // set to 1.0 because composition is one of the fundamental components - IntroducedVersion: version.MajorMinor(1, 0), - EnvOptions: []cel.EnvOption{ - cel.Variable("variables", declType.CelType()), - }, - DeclTypes: []*apiservercel.DeclType{ - declType, - }, - }) - if err != nil { - return nil, err - } - return &CompositionEnv{ - MapType: declType, - EnvSet: envSet, - CompiledVariables: map[string]CompilationResult{}, - }, nil +func (c *compositionState) AddField(name string, celType *cel.Type) { + c.mapType.Fields[name] = apiservercel.NewDeclField(name, convertCelTypeToDeclType(celType), true, nil, nil) } -func (c *CompositionEnv) CreateContext(parent context.Context) CompositionContext { +func (c *compositionState) CreateContext(parent context.Context) CompositionContext { return &compositionContext{ - Context: parent, - compositionEnv: c, + Context: parent, + state: c, } } @@ -165,13 +189,13 @@ type CompositionContext interface { type compositionContext struct { context.Context - compositionEnv *CompositionEnv + state *compositionState accumulatedCost int64 } func (c *compositionContext) Variables(activation any) ref.Val { - lazyMap := lazy.NewMapValue(c.compositionEnv.MapType) - for name, result := range c.compositionEnv.CompiledVariables { + lazyMap := lazy.NewMapValue(c.state.mapType) + for name, result := range c.state.compiledVariables { accessor := &variableAccessor{ name: name, result: result, @@ -184,7 +208,7 @@ func (c *compositionContext) Variables(activation any) ref.Val { } func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { - ctx = f.compositionEnv.CreateContext(ctx) + ctx = f.state.CreateContext(ctx) return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition_test.go similarity index 74% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition_test.go index 13a373faa..45859f5c2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/composition_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/composition_test.go @@ -48,15 +48,14 @@ func (t *testVariable) GetName() string { func TestCompositedPolicies(t *testing.T) { cases := []struct { - name string - variables []NamedExpressionAccessor - expression string - attributes admission.Attributes - expectedResult any - expectErr bool - expectedErrorMessage string - runtimeCostBudget int64 - strictCostEnforcement bool + name string + variables []NamedExpressionAccessor + expression string + attributes admission.Attributes + expectedResult any + expectErr bool + expectedErrorMessage string + runtimeCostBudget int64 }{ { name: "simple", @@ -187,44 +186,29 @@ func TestCompositedPolicies(t *testing.T) { expectedErrorMessage: "found no matching overload for '_==_' applied to '(string, int)'", }, { - name: "with strictCostEnforcement on: exceeds cost budget", + name: "exceeds cost budget", variables: []NamedExpressionAccessor{ &testVariable{ name: "dict", expression: "'abc 123 def 123'.split(' ')", }, }, - attributes: endpointCreateAttributes(), - expression: "size(variables.dict) > 0", - expectErr: true, - expectedErrorMessage: "validation failed due to running out of cost budget, no further validation rules will be run", - runtimeCostBudget: 5, - strictCostEnforcement: true, - }, - { - name: "with strictCostEnforcement off: not exceed cost budget", - variables: []NamedExpressionAccessor{ - &testVariable{ - name: "dict", - expression: "'abc 123 def 123'.split(' ')", - }, - }, - attributes: endpointCreateAttributes(), - expression: "size(variables.dict) > 0", - expectedResult: true, - runtimeCostBudget: 5, - strictCostEnforcement: false, + attributes: endpointCreateAttributes(), + expression: "size(variables.dict) > 0", + expectErr: true, + expectedErrorMessage: "validation failed due to running out of cost budget, no further validation rules will be run", + runtimeCostBudget: 5, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - compiler, err := NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCostEnforcement)) + compiler, err := NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) if err != nil { t.Fatal(err) } - compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) + compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) validations := []ExpressionAccessor{&testCondition{Expression: tc.expression}} - f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) + f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) if err != nil { t.Fatal(err) @@ -263,3 +247,46 @@ func TestCompositedPolicies(t *testing.T) { }) } } + +// TestCompilerIsolation verifies that each call to NewCompositedCompiler +// creates an isolated environment where variables from one compiler +// do not leak into another. +func TestCompilerIsolation(t *testing.T) { + baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) + + // Create first compiler with variable "foo" + compiler1, err := NewCompositedCompiler(baseEnv) + if err != nil { + t.Fatal(err) + } + vars1 := []NamedExpressionAccessor{ + &testVariable{name: "foo", expression: "'bar'"}, + } + compiler1.CompileAndStoreVariables(vars1, OptionalVariableDeclarations{}, environment.StoredExpressions) + + // Create second compiler with variable "baz" + compiler2, err := NewCompositedCompiler(baseEnv) + if err != nil { + t.Fatal(err) + } + vars2 := []NamedExpressionAccessor{ + &testVariable{name: "baz", expression: "'qux'"}, + } + compiler2.CompileAndStoreVariables(vars2, OptionalVariableDeclarations{}, environment.StoredExpressions) + + // Verify isolation: compiler1 should not have "baz", compiler2 should not have "foo" + if _, ok := compiler1.state.mapType.Fields["baz"]; ok { + t.Error("compiler1 should not have variable 'baz' from compiler2") + } + if _, ok := compiler2.state.mapType.Fields["foo"]; ok { + t.Error("compiler2 should not have variable 'foo' from compiler1") + } + + // Verify each compiler has its own variable + if _, ok := compiler1.state.mapType.Fields["foo"]; !ok { + t.Error("compiler1 should have variable 'foo'") + } + if _, ok := compiler2.state.mapType.Fields["baz"]; !ok { + t.Error("compiler2 should have variable 'baz'") + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/condition.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/condition.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/condition.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/condition.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/condition_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/condition_test.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/condition_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/condition_test.go index 046a0db38..5cf898a90 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/condition_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/condition_test.go @@ -99,8 +99,8 @@ func TestCompile(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))} - e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions) + c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))} + e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions) if e == nil { t.Fatalf("unexpected nil validator") } @@ -197,7 +197,6 @@ func TestCondition(t *testing.T) { authorizer authorizer.Authorizer testPerCallLimit uint64 namespaceObject *corev1.Namespace - strictCost bool enableSelectors bool compatibilityVersion *version.Version @@ -804,7 +803,7 @@ func TestCondition(t *testing.T) { attributes: newValidAttribute(nil, false), results: []EvaluationResult{ { - Error: errors.New(fmt.Sprintf("operation cancelled: actual cost limit exceeded")), + Error: fmt.Errorf("operation cancelled: actual cost limit exceeded"), }, }, hasParamKind: true, @@ -932,7 +931,7 @@ func TestCondition(t *testing.T) { if compatibilityVersion == nil { compatibilityVersion = environment.DefaultCompatibilityVersion() } - env, err := environment.MustBaseEnvSet(compatibilityVersion, tc.strictCost).Extend( + env, err := environment.MustBaseEnvSet(compatibilityVersion).Extend( environment.VersionedOptions{ IntroducedVersion: compatibilityVersion, ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)}, @@ -946,7 +945,7 @@ func TestCondition(t *testing.T) { if envType == "" { envType = environment.NewExpressions } - f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, envType) + f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, envType) if f == nil { t.Fatalf("unexpected nil validator") } @@ -989,6 +988,8 @@ func TestCondition(t *testing.T) { } func TestRuntimeCELCostBudget(t *testing.T) { + checkCost := int64(350000) + configMapParams := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", @@ -999,17 +1000,16 @@ func TestRuntimeCELCostBudget(t *testing.T) { } cases := []struct { - name string - attributes admission.Attributes - params runtime.Object - validations []ExpressionAccessor - hasParamKind bool - authorizer authorizer.Authorizer - testRuntimeCELCostBudget int64 - exceedBudget bool - expectRemainingBudget *int64 - enableStrictCostEnforcement bool - exceedPerCallLimit bool + name string + attributes admission.Attributes + params runtime.Object + validations []ExpressionAccessor + hasParamKind bool + authorizer authorizer.Authorizer + testRuntimeCELCostBudget int64 + exceedBudget bool + expectRemainingBudget *int64 + exceedPerCallLimit bool }{ { name: "expression exceed RuntimeCELCostBudget at fist expression", @@ -1106,7 +1106,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 6, + testRuntimeCELCostBudget: checkCost + 5, expectRemainingBudget: pointer.To(int64(0)), authorizer: denyAll, }, @@ -1120,7 +1120,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 1, + testRuntimeCELCostBudget: 4, expectRemainingBudget: pointer.To(int64(0)), }, { @@ -1133,7 +1133,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 2, + testRuntimeCELCostBudget: 4, expectRemainingBudget: pointer.To(int64(0)), }, { @@ -1146,7 +1146,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 3, + testRuntimeCELCostBudget: 5, expectRemainingBudget: pointer.To(int64(0)), }, { @@ -1159,7 +1159,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 3, + testRuntimeCELCostBudget: 7, expectRemainingBudget: pointer.To(int64(0)), }, { @@ -1172,7 +1172,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { attributes: newValidAttribute(nil, false), hasParamKind: false, exceedBudget: false, - testRuntimeCELCostBudget: 3, + testRuntimeCELCostBudget: 4, expectRemainingBudget: pointer.To(int64(0)), }, { @@ -1198,12 +1198,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "has(object.subsets)", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - testRuntimeCELCostBudget: 35000, - exceedBudget: true, - authorizer: denyAll, - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + testRuntimeCELCostBudget: 35000, + exceedBudget: true, + authorizer: denyAll, }, { name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression", @@ -1215,12 +1214,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - testRuntimeCELCostBudget: 700000, - exceedBudget: true, - authorizer: denyAll, - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + testRuntimeCELCostBudget: 700000, + exceedBudget: true, + authorizer: denyAll, }, { name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed", @@ -1232,14 +1230,13 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: true, - params: configMapParams, - exceedBudget: false, - testRuntimeCELCostBudget: 700011, - expectRemainingBudget: pointer.To(int64(1)), // 700011 - 700010 - authorizer: denyAll, - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: true, + params: configMapParams, + exceedBudget: false, + testRuntimeCELCostBudget: 700011, + expectRemainingBudget: pointer.To(int64(1)), // 700011 - 700010 + authorizer: denyAll, }, { name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers", @@ -1251,14 +1248,13 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: true, - params: configMapParams, - exceedBudget: false, - testRuntimeCELCostBudget: 700010, - expectRemainingBudget: pointer.To(int64(0)), - authorizer: denyAll, - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: true, + params: configMapParams, + exceedBudget: false, + testRuntimeCELCostBudget: 700010, + expectRemainingBudget: pointer.To(int64(0)), + authorizer: denyAll, }, { name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds", @@ -1267,13 +1263,12 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: true, - params: configMapParams, - authorizer: denyAll, - exceedPerCallLimit: true, - testRuntimeCELCostBudget: -1, - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: true, + params: configMapParams, + authorizer: denyAll, + exceedPerCallLimit: true, + testRuntimeCELCostBudget: -1, }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()", @@ -1282,12 +1277,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "[1,2,3,4].isSorted()", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 4, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 4, + expectRemainingBudget: pointer.To(int64(0)), }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url", @@ -1296,12 +1290,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 4, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 4, + expectRemainingBudget: pointer.To(int64(0)), }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split", @@ -1310,12 +1303,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "size('abc 123 def 123'.split(' ')) > 0", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 5, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 5, + expectRemainingBudget: pointer.To(int64(0)), }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join", @@ -1324,12 +1316,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 7, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 7, + expectRemainingBudget: pointer.To(int64(0)), }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find", @@ -1338,12 +1329,11 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "size('abc 123 def 123'.find('123')) > 0", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 4, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 4, + expectRemainingBudget: pointer.To(int64(0)), }, { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity", @@ -1352,19 +1342,18 @@ func TestRuntimeCELCostBudget(t *testing.T) { Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")", }, }, - attributes: newValidAttribute(nil, false), - hasParamKind: false, - exceedBudget: false, - testRuntimeCELCostBudget: 6, - expectRemainingBudget: pointer.To(int64(0)), - enableStrictCostEnforcement: true, + attributes: newValidAttribute(nil, false), + hasParamKind: false, + exceedBudget: false, + testRuntimeCELCostBudget: 6, + expectRemainingBudget: pointer.To(int64(0)), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))} - f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions) + c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))} + f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true}, environment.NewExpressions) if f == nil { t.Fatalf("unexpected nil validator") } @@ -1584,6 +1573,8 @@ func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) panic(fmt.Sprintf("unsupported type: %T", a)) } + // Compare AttributesRecord structs - contains error fields but we're comparing the struct, not handling errors + //nolint:all if reflect.DeepEqual(f.match.match, *other) { return f.match.decision, f.match.reason, f.match.err } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/interface.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/interface.go index a9e35a226..55faa34ec 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/interface.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/interface.go @@ -61,8 +61,6 @@ type OptionalVariableDeclarations struct { // variables are declared. When declared, the authorizer variables are // expected to be non-null. HasAuthorizer bool - // StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries. - StrictCost bool // HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used // to initialize the typed objects in CEL required to create patches. HasPatchTypes bool diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/mutation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/mutation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/cel/mutation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/cel/mutation.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader.go new file mode 100644 index 000000000..bb160fc10 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader.go @@ -0,0 +1,120 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package manifest provides shared utilities for loading admission configurations +// from static manifest files. +package manifest + +import ( + "bufio" + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + utilyaml "k8s.io/apimachinery/pkg/util/yaml" +) + +// splitYAMLDocuments splits a multi-document YAML byte slice into individual documents. +// Empty documents are skipped. +func splitYAMLDocuments(data []byte) ([][]byte, error) { + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data))) + var docs [][]byte + for { + doc, err := reader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + doc = bytes.TrimSpace(doc) + if len(doc) == 0 { + continue + } + docs = append(docs, doc) + } + return docs, nil +} + +// FileDoc holds a decoded YAML document and the file it came from. +type FileDoc struct { + FilePath string + Doc []byte +} + +// LoadFiles reads all YAML/JSON files from dir, splits multi-document YAML, +// and returns individual documents with their source file paths plus a +// sha256-prefixed hash of the file contents for change detection. +// Files are processed in alphabetical order for deterministic behavior. +func LoadFiles(dir string) ([]FileDoc, string, error) { + if len(dir) == 0 { + return nil, "", fmt.Errorf("manifest directory path is empty") + } + + // os.ReadDir returns entries sorted by filename. + entries, err := os.ReadDir(dir) + if err != nil { + return nil, "", fmt.Errorf("failed to read manifest directory %q: %w", dir, err) + } + + var fileDocs []FileDoc + h := sha256.New() + hasData := false + + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + ext := strings.ToLower(filepath.Ext(name)) + if ext != ".yaml" && ext != ".yml" && ext != ".json" { + continue + } + + filePath := filepath.Join(dir, name) + data, err := os.ReadFile(filePath) + if err != nil { + return nil, "", fmt.Errorf("failed to read file %q: %w", filePath, err) + } + if len(data) == 0 { + continue + } + + h.Write(data) + h.Write([]byte{0}) + hasData = true + + docs, err := splitYAMLDocuments(data) + if err != nil { + return nil, "", fmt.Errorf("failed to split YAML documents in file %q: %w", filePath, err) + } + for _, doc := range docs { + fileDocs = append(fileDocs, FileDoc{FilePath: filePath, Doc: doc}) + } + } + + var hash string + if hasData { + hash = fmt.Sprintf("sha256:%x", h.Sum(nil)) + } + + return fileDocs, hash, nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader_test.go new file mode 100644 index 000000000..8c8172498 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/loader_test.go @@ -0,0 +1,281 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifest + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func Test_splitYAMLDocuments(t *testing.T) { + tests := []struct { + name string + input string + wantCount int + wantErr bool + // wantContains checks that each returned doc contains the expected substring + wantContains []string + }{ + { + name: "empty input", + input: "", + wantCount: 0, + }, + { + name: "only whitespace", + input: " \n\n \n", + wantCount: 0, + }, + { + name: "single YAML webhook configuration", + input: `apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: test-webhook.static.k8s.io +webhooks: +- name: test.example.com + clientConfig: + url: "https://example.com/validate" +`, + wantCount: 1, + wantContains: []string{"ValidatingWebhookConfiguration"}, + }, + { + name: "multi-document YAML with policy and binding", + input: `apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: deny-privileged.static.k8s.io +spec: + failurePolicy: Fail +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: deny-privileged-binding.static.k8s.io +spec: + policyName: deny-privileged.static.k8s.io +`, + wantCount: 2, + wantContains: []string{"ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"}, + }, + { + name: "single JSON document", + input: `{"apiVersion":"admissionregistration.k8s.io/v1","kind":"ValidatingWebhookConfiguration","metadata":{"name":"test.static.k8s.io"}}`, + wantCount: 1, + wantContains: []string{"ValidatingWebhookConfiguration"}, + }, + { + name: "empty document between separators is skipped", + input: `a: 1 +--- +--- +b: 2 +`, + wantCount: 2, + }, + { + name: "leading separator", + input: `--- +apiVersion: v1 +kind: ConfigMap +`, + wantCount: 1, + }, + { + name: "trailing separator", + input: `apiVersion: v1 +kind: ConfigMap +--- +`, + wantCount: 1, + }, + { + name: "three documents", + input: `kind: A +--- +kind: B +--- +kind: C +`, + wantCount: 3, + wantContains: []string{"A", "B", "C"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + docs, err := splitYAMLDocuments([]byte(tc.input)) + if (err != nil) != tc.wantErr { + t.Fatalf("splitYAMLDocuments() error = %v, wantErr %v", err, tc.wantErr) + } + if len(docs) != tc.wantCount { + t.Fatalf("splitYAMLDocuments() returned %d documents, want %d", len(docs), tc.wantCount) + } + // Verify no empty documents were returned + for i, doc := range docs { + trimmed := strings.TrimSpace(string(doc)) + if trimmed == "" { + t.Errorf("document %d is empty after trim", i) + } + } + // Verify expected content + for i, want := range tc.wantContains { + if i >= len(docs) { + break + } + if !strings.Contains(string(docs[i]), want) { + t.Errorf("document %d: expected to contain %q, got %q", i, want, string(docs[i])) + } + } + }) + } +} + +func TestLoadFiles(t *testing.T) { + t.Run("empty dir returns no docs", func(t *testing.T) { + dir := t.TempDir() + docs, hash, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 0 { + t.Errorf("expected 0 docs, got %d", len(docs)) + } + if hash != "" { + t.Errorf("expected empty hash for empty dir, got %q", hash) + } + }) + + t.Run("reads yaml and json files", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "a.yaml"), []byte("kind: A"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "b.json"), []byte(`{"kind":"B"}`), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "c.yml"), []byte("kind: C"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "d.txt"), []byte("ignored"), 0644); err != nil { + t.Fatal(err) + } + + docs, hash, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 3 { + t.Fatalf("expected 3 docs (.yaml, .json, .yml), got %d", len(docs)) + } + if len(hash) == 0 { + t.Error("expected non-empty hash") + } + }) + + t.Run("splits multi-document yaml", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "multi.yaml"), []byte("a: 1\n---\nb: 2\n"), 0644); err != nil { + t.Fatal(err) + } + + docs, _, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 2 { + t.Fatalf("expected 2 docs, got %d", len(docs)) + } + }) + + t.Run("alphabetical order", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "02-second.yaml"), []byte("name: second"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "01-first.yaml"), []byte("name: first"), 0644); err != nil { + t.Fatal(err) + } + + docs, _, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 2 { + t.Fatalf("expected 2 docs, got %d", len(docs)) + } + if !strings.Contains(string(docs[0].Doc), "first") { + t.Errorf("first doc should be from 01-first.yaml, got %q", string(docs[0].Doc)) + } + }) + + t.Run("empty path returns error", func(t *testing.T) { + _, _, err := LoadFiles("") + if err == nil { + t.Error("expected error for empty path") + } + }) + + t.Run("nonexistent dir returns error", func(t *testing.T) { + _, _, err := LoadFiles("/nonexistent/path") + if err == nil { + t.Error("expected error for nonexistent dir") + } + }) + + t.Run("skips subdirectories", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "a.yaml"), []byte("kind: A"), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "subdir"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "subdir", "b.yaml"), []byte("kind: B"), 0644); err != nil { + t.Fatal(err) + } + + docs, _, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 1 { + t.Fatalf("expected 1 doc (subdir skipped), got %d", len(docs)) + } + }) + + t.Run("skips empty files", func(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "empty.yaml"), []byte(""), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "valid.yaml"), []byte("kind: A"), 0644); err != nil { + t.Fatal(err) + } + + docs, _, err := LoadFiles(dir) + if err != nil { + t.Fatal(err) + } + if len(docs) != 1 { + t.Fatalf("expected 1 doc (empty skipped), got %d", len(docs)) + } + }) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics.go new file mode 100644 index 000000000..efdf2fe21 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics.go @@ -0,0 +1,161 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package metrics provides metrics for manifest-based admission configuration. +package metrics + +import ( + "crypto/sha256" + "fmt" + "sync" + + "k8s.io/apiserver/pkg/util/configmetrics" + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +const ( + namespace = "apiserver" + subsystem = "manifest_admission_config_controller" +) + +var ( + admissionManifestAutomaticReloadsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "automatic_reloads_total", + Help: "Total number of automatic reloads of admission manifest configuration split by status, plugin, and apiserver identity.", + StabilityLevel: metrics.ALPHA, + }, + []string{"status", "plugin", "apiserver_id_hash"}, + ) + + admissionManifestAutomaticReloadLastTimestampSeconds = metrics.NewGaugeVec( + &metrics.GaugeOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "automatic_reload_last_timestamp_seconds", + Help: "Timestamp of the last automatic reload of admission manifest configuration split by status, plugin, and apiserver identity.", + StabilityLevel: metrics.ALPHA, + }, + []string{"status", "plugin", "apiserver_id_hash"}, + ) + + admissionManifestLastConfigInfo = metrics.NewDesc( + metrics.BuildFQName(namespace, subsystem, "last_config_info"), + "Information about the last applied admission manifest configuration with hash as label, split by plugin and apiserver identity.", + []string{"plugin", "apiserver_id_hash", "hash"}, + nil, + metrics.ALPHA, + "", + ) +) + +// ManifestType represents the admission plugin name for which manifests are being tracked. +type ManifestType string + +const ( + // ValidatingWebhookManifestType represents validating webhook configurations. + ValidatingWebhookManifestType ManifestType = "ValidatingAdmissionWebhook" + // MutatingWebhookManifestType represents mutating webhook configurations. + MutatingWebhookManifestType ManifestType = "MutatingAdmissionWebhook" + // VAPManifestType represents validating admission policy configurations. + VAPManifestType ManifestType = "ValidatingAdmissionPolicy" + // MAPManifestType represents mutating admission policy configurations. + MAPManifestType ManifestType = "MutatingAdmissionPolicy" +) + +var registerMetrics sync.Once + +// configHashProviders stores providers per plugin for config info metrics. +// Each plugin has its own provider that stores [apiserver_id_hash, config_hash]. +var configHashProviders = map[ManifestType]*configmetrics.AtomicHashProvider{ + ValidatingWebhookManifestType: configmetrics.NewAtomicHashProvider(), + MutatingWebhookManifestType: configmetrics.NewAtomicHashProvider(), + VAPManifestType: configmetrics.NewAtomicHashProvider(), + MAPManifestType: configmetrics.NewAtomicHashProvider(), +} + +// multiTypeConfigInfoCollector emits config info metrics for multiple manifest types. +type multiTypeConfigInfoCollector struct { + metrics.BaseStableCollector + desc *metrics.Desc +} + +var _ metrics.StableCollector = &multiTypeConfigInfoCollector{} + +func (c *multiTypeConfigInfoCollector) DescribeWithStability(ch chan<- *metrics.Desc) { + ch <- c.desc +} + +func (c *multiTypeConfigInfoCollector) CollectWithStability(ch chan<- metrics.Metric) { + // Emit a metric for each plugin that has data + for manifestType, provider := range configHashProviders { + hashes := provider.GetCurrentHashes() + if len(hashes) >= 2 && len(hashes[0]) > 0 { + // hashes contains [apiserver_id_hash, config_hash] + // Prepend the plugin to match label order: ["plugin", "apiserver_id_hash", "hash"] + labelValues := append([]string{string(manifestType)}, hashes...) + ch <- metrics.NewLazyConstMetric(c.desc, metrics.GaugeValue, 1, labelValues...) + } + } +} + +func RegisterMetrics() { + registerMetrics.Do(func() { + legacyregistry.MustRegister(admissionManifestAutomaticReloadsTotal) + legacyregistry.MustRegister(admissionManifestAutomaticReloadLastTimestampSeconds) + // Use a custom collector that emits metrics for all manifest types + legacyregistry.CustomMustRegister(&multiTypeConfigInfoCollector{desc: admissionManifestLastConfigInfo}) + }) +} + +func ResetMetricsForTest() { + admissionManifestAutomaticReloadsTotal.Reset() + admissionManifestAutomaticReloadLastTimestampSeconds.Reset() +} + +// RecordAutomaticReloadFailure records a failed reload attempt for the given manifest type. +func RecordAutomaticReloadFailure(manifestType ManifestType, apiServerID string) { + apiServerIDHash := getHash(apiServerID) + admissionManifestAutomaticReloadsTotal.WithLabelValues("failure", string(manifestType), apiServerIDHash).Inc() + admissionManifestAutomaticReloadLastTimestampSeconds.WithLabelValues("failure", string(manifestType), apiServerIDHash).SetToCurrentTime() +} + +// RecordAutomaticReloadSuccess records a successful reload for the given manifest type. +func RecordAutomaticReloadSuccess(manifestType ManifestType, apiServerID, configHash string) { + apiServerIDHash := getHash(apiServerID) + admissionManifestAutomaticReloadsTotal.WithLabelValues("success", string(manifestType), apiServerIDHash).Inc() + admissionManifestAutomaticReloadLastTimestampSeconds.WithLabelValues("success", string(manifestType), apiServerIDHash).SetToCurrentTime() + + RecordLastConfigInfo(manifestType, apiServerID, configHash) +} + +// RecordLastConfigInfo records the hash of the last successfully loaded configuration. +func RecordLastConfigInfo(manifestType ManifestType, apiServerID, configHash string) { + if provider, ok := configHashProviders[manifestType]; ok { + // Store [apiserver_id_hash, config_hash] - the multiTypeConfigInfoCollector prepends the type + provider.SetHashes(getHash(apiServerID), configHash) + } +} + +func getHash(data string) string { + if len(data) == 0 { + return "" + } + return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(data))) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics_test.go new file mode 100644 index 000000000..7dba8000b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/metrics/metrics_test.go @@ -0,0 +1,247 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "strings" + "testing" + + "k8s.io/component-base/metrics" +) + +func TestGetHash(t *testing.T) { + tests := []struct { + name string + input string + want string // empty means expect empty return + }{ + { + name: "empty string returns empty", + input: "", + want: "", + }, + { + name: "non-empty string returns sha256 prefix", + input: "test-data", + }, + { + name: "different inputs produce different hashes", + input: "other-data", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := getHash(tc.input) + if tc.want == "" && tc.input == "" { + if result != "" { + t.Errorf("getHash(%q) = %q, want empty", tc.input, result) + } + return + } + if !strings.HasPrefix(result, "sha256:") { + t.Errorf("getHash(%q) = %q, want sha256: prefix", tc.input, result) + } + }) + } + + // Verify determinism + hash1 := getHash("same-input") + hash2 := getHash("same-input") + if hash1 != hash2 { + t.Errorf("getHash is not deterministic: %q != %q", hash1, hash2) + } + + // Verify different inputs produce different hashes + hashA := getHash("input-a") + hashB := getHash("input-b") + if hashA == hashB { + t.Errorf("different inputs produced same hash: %q", hashA) + } +} + +func TestRecordAutomaticReloadSuccess(t *testing.T) { + RegisterMetrics() + ResetMetricsForTest() + + RecordAutomaticReloadSuccess(ValidatingWebhookManifestType, "test-server-id", "test-config-data") + + // Verify config info was recorded + provider := configHashProviders[ValidatingWebhookManifestType] + hashes := provider.GetCurrentHashes() + if len(hashes) < 2 { + t.Fatalf("expected at least 2 hashes, got %d", len(hashes)) + } + if hashes[0] == "" { + t.Error("expected non-empty apiserver ID hash") + } + if hashes[1] == "" { + t.Error("expected non-empty config hash") + } +} + +func TestRecordAutomaticReloadFailure(t *testing.T) { + RegisterMetrics() + ResetMetricsForTest() + + // Should not panic + RecordAutomaticReloadFailure(MutatingWebhookManifestType, "test-server-id") +} + +func TestRecordLastConfigInfo(t *testing.T) { + RegisterMetrics() + + tests := []struct { + name string + manifestType ManifestType + apiServerID string + configData string + }{ + { + name: "validating webhook", + manifestType: ValidatingWebhookManifestType, + apiServerID: "server-1", + configData: "config-1", + }, + { + name: "mutating webhook", + manifestType: MutatingWebhookManifestType, + apiServerID: "server-2", + configData: "config-2", + }, + { + name: "VAP", + manifestType: VAPManifestType, + apiServerID: "server-3", + configData: "config-3", + }, + { + name: "MAP", + manifestType: MAPManifestType, + apiServerID: "server-4", + configData: "config-4", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + RecordLastConfigInfo(tc.manifestType, tc.apiServerID, tc.configData) + + provider := configHashProviders[tc.manifestType] + hashes := provider.GetCurrentHashes() + if len(hashes) < 2 { + t.Fatalf("expected at least 2 hashes, got %d", len(hashes)) + } + expectedAPIHash := getHash(tc.apiServerID) + if hashes[0] != expectedAPIHash { + t.Errorf("apiserver hash = %q, want %q", hashes[0], expectedAPIHash) + } + // configData is already a hash, stored directly + if hashes[1] != tc.configData { + t.Errorf("config hash = %q, want %q", hashes[1], tc.configData) + } + }) + } +} + +func TestRecordLastConfigInfoUnknownType(t *testing.T) { + // Should not panic for unknown manifest type + RecordLastConfigInfo(ManifestType("Unknown"), "server", "data") +} + +func TestMultiTypeConfigInfoCollectorDescribe(t *testing.T) { + collector := &multiTypeConfigInfoCollector{desc: admissionManifestLastConfigInfo} + ch := make(chan *metrics.Desc, 1) + collector.DescribeWithStability(ch) + close(ch) + + var descs []*metrics.Desc + for d := range ch { + descs = append(descs, d) + } + if len(descs) != 1 { + t.Fatalf("expected 1 descriptor, got %d", len(descs)) + } + if descs[0] != admissionManifestLastConfigInfo { + t.Error("descriptor does not match admissionManifestLastConfigInfo") + } +} + +func TestMultiTypeConfigInfoCollectorCollectEmpty(t *testing.T) { + // Reset all providers to empty state + for _, provider := range configHashProviders { + provider.SetHashes() + } + + collector := &multiTypeConfigInfoCollector{desc: admissionManifestLastConfigInfo} + ch := make(chan metrics.Metric, 10) + collector.CollectWithStability(ch) + close(ch) + + var collected []metrics.Metric + for m := range ch { + collected = append(collected, m) + } + if len(collected) != 0 { + t.Errorf("expected 0 metrics when no hashes are set, got %d", len(collected)) + } +} + +func TestMultiTypeConfigInfoCollectorCollectWithData(t *testing.T) { + // Reset all providers + for _, provider := range configHashProviders { + provider.SetHashes() + } + + // Set data for two types + RecordLastConfigInfo(ValidatingWebhookManifestType, "server-a", "config-a") + RecordLastConfigInfo(VAPManifestType, "server-b", "config-b") + + collector := &multiTypeConfigInfoCollector{desc: admissionManifestLastConfigInfo} + ch := make(chan metrics.Metric, 10) + collector.CollectWithStability(ch) + close(ch) + + var collected []metrics.Metric + for m := range ch { + collected = append(collected, m) + } + if len(collected) != 2 { + t.Fatalf("expected 2 metrics for 2 types with data, got %d", len(collected)) + } +} + +func TestMultiTypeConfigInfoCollectorCollectAllTypes(t *testing.T) { + // Set data for all four types + RecordLastConfigInfo(ValidatingWebhookManifestType, "s1", "c1") + RecordLastConfigInfo(MutatingWebhookManifestType, "s2", "c2") + RecordLastConfigInfo(VAPManifestType, "s3", "c3") + RecordLastConfigInfo(MAPManifestType, "s4", "c4") + + collector := &multiTypeConfigInfoCollector{desc: admissionManifestLastConfigInfo} + ch := make(chan metrics.Metric, 10) + collector.CollectWithStability(ch) + close(ch) + + var collected []metrics.Metric + for m := range ch { + collected = append(collected, m) + } + if len(collected) != 4 { + t.Fatalf("expected 4 metrics for all types, got %d", len(collected)) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation.go new file mode 100644 index 000000000..9effc544f --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation.go @@ -0,0 +1,74 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package manifest provides shared utilities for loading admission configurations +// from static manifest files. +package manifest + +import ( + "fmt" + "os" + "path" + "strings" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" +) + +// StaticConfigSuffix is the reserved suffix for manifest-based admission configurations. +// Resources with names ending in this suffix can only be created via static manifest +// files loaded at API server startup, not through the REST API. +// NOTE: This constant is duplicated in pkg/apis/admissionregistration/validation/static_suffix.go +// because that package cannot import from staging. Keep both in sync. +const StaticConfigSuffix = ".static.k8s.io" + +// ValidateStaticManifestsDir validates the staticManifestsDir config field. +// It checks the feature gate is enabled, the path is absolute, exists, and is a directory. +func ValidateStaticManifestsDir(staticManifestsDir string) error { + if len(staticManifestsDir) > 0 { + if !utilfeature.DefaultFeatureGate.Enabled(features.ManifestBasedAdmissionControlConfig) { + return field.Forbidden(field.NewPath("staticManifestsDir"), "staticManifestsDir requires the ManifestBasedAdmissionControlConfig feature gate to be enabled") + } + if !path.IsAbs(staticManifestsDir) { + return field.Invalid(field.NewPath("staticManifestsDir"), staticManifestsDir, "must be an absolute file path") + } + info, err := os.Stat(staticManifestsDir) + if err != nil { + return field.Invalid(field.NewPath("staticManifestsDir"), staticManifestsDir, fmt.Sprintf("unable to read: %v", err)) + } + if !info.IsDir() { + return field.Invalid(field.NewPath("staticManifestsDir"), staticManifestsDir, "must be a directory") + } + } + return nil +} + +// ValidateManifestName checks that the object name is non-empty, has the required +// .static.k8s.io suffix, and is unique within the manifest set. +func ValidateManifestName(name, filePath string, seenNames map[string]string) error { + if len(name) == 0 { + return fmt.Errorf("resource in file %q must have a name", filePath) + } + if !strings.HasSuffix(name, StaticConfigSuffix) { + return fmt.Errorf("%q in file %q must have a name ending with %q", name, filePath, StaticConfigSuffix) + } + if prevFile, ok := seenNames[name]; ok { + return fmt.Errorf("duplicate name %q found in file %q (previously seen in %q)", name, filePath, prevFile) + } + seenNames[name] = filePath + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation_test.go new file mode 100644 index 000000000..637690c31 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/manifest/validation_test.go @@ -0,0 +1,156 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifest + +import ( + "os" + "path" + "strings" + "testing" + + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func TestValidateStaticManifestsDir(t *testing.T) { + validDir := t.TempDir() + notADirFile := path.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(notADirFile, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + dir string + enableFeatureGate bool + wantErr string + }{ + { + name: "empty dir is valid", + dir: "", + enableFeatureGate: true, + }, + { + name: "valid directory", + dir: validDir, + enableFeatureGate: true, + }, + { + name: "feature gate disabled", + dir: "/some/path", + enableFeatureGate: false, + wantErr: "Forbidden", + }, + { + name: "relative path", + dir: "relative/path", + enableFeatureGate: true, + wantErr: "must be an absolute file path", + }, + { + name: "nonexistent path", + dir: "/nonexistent/path", + enableFeatureGate: true, + wantErr: "unable to read", + }, + { + name: "not a directory", + dir: notADirFile, + enableFeatureGate: true, + wantErr: "must be a directory", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ManifestBasedAdmissionControlConfig, tc.enableFeatureGate) + err := ValidateStaticManifestsDir(tc.dir) + if len(tc.wantErr) > 0 { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("expected error containing %q, got: %v", tc.wantErr, err) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestValidateManifestName(t *testing.T) { + tests := []struct { + name string + objName string + filePath string + seenNames map[string]string + wantErr bool + errContains string + }{ + { + name: "valid name", + objName: "test.static.k8s.io", + filePath: "test.yaml", + seenNames: map[string]string{}, + }, + { + name: "empty name", + objName: "", + filePath: "test.yaml", + seenNames: map[string]string{}, + wantErr: true, + errContains: "must have a name", + }, + { + name: "missing suffix", + objName: "no-suffix", + filePath: "test.yaml", + seenNames: map[string]string{}, + wantErr: true, + errContains: "must have a name ending with", + }, + { + name: "duplicate name", + objName: "dup.static.k8s.io", + filePath: "second.yaml", + seenNames: map[string]string{"dup.static.k8s.io": "first.yaml"}, + wantErr: true, + errContains: "duplicate", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateManifestName(tt.objName, tt.filePath, tt.seenNames) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission.go index 936a95e45..12654e271 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission.go @@ -54,7 +54,7 @@ const ( // Register registers a plugin func Register(plugins *admission.Plugins) { plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { - return NewLifecycle(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic)) + return NewLifecycle(sets.New[string](metav1.NamespaceDefault, metav1.NamespaceSystem, metav1.NamespacePublic)) }) } @@ -63,7 +63,7 @@ func Register(plugins *admission.Plugins) { type Lifecycle struct { *admission.Handler client kubernetes.Interface - immortalNamespaces sets.String + immortalNamespaces sets.Set[string] namespaceLister corelisters.NamespaceLister // forceLiveLookupCache holds a list of entries for namespaces that we have a strong reason to believe are stale in our local cache. // if a namespace is in this cache, then we will ignore our local state and always fetch latest from api server. @@ -186,11 +186,11 @@ func (l *Lifecycle) Admit(ctx context.Context, a admission.Attributes, o admissi } // NewLifecycle creates a new namespace Lifecycle admission control handler -func NewLifecycle(immortalNamespaces sets.String) (*Lifecycle, error) { +func NewLifecycle(immortalNamespaces sets.Set[string]) (*Lifecycle, error) { return newLifecycleWithClock(immortalNamespaces, clock.RealClock{}) } -func newLifecycleWithClock(immortalNamespaces sets.String, clock utilcache.Clock) (*Lifecycle, error) { +func newLifecycleWithClock(immortalNamespaces sets.Set[string], clock utilcache.Clock) (*Lifecycle, error) { forceLiveLookupCache := utilcache.NewLRUExpireCacheWithClock(100, clock) return &Lifecycle{ Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete), diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go index 22c693d2d..debb61e2b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/namespace/lifecycle/admission_test.go @@ -24,6 +24,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -49,11 +50,11 @@ func newHandlerForTest(c clientset.Interface) (*Lifecycle, informers.SharedInfor // newHandlerForTestWithClock returns a configured handler for testing. func newHandlerForTestWithClock(c clientset.Interface, cacheClock clock.Clock) (*Lifecycle, informers.SharedInformerFactory, error) { f := informers.NewSharedInformerFactory(c, 5*time.Minute) - handler, err := newLifecycleWithClock(sets.NewString(metav1.NamespaceDefault, metav1.NamespaceSystem), cacheClock) + handler, err := newLifecycleWithClock(sets.New[string](metav1.NamespaceDefault, metav1.NamespaceSystem), cacheClock) if err != nil { return nil, f, err } - pluginInitializer := kubeadmission.New(c, nil, f, nil, nil, nil, nil) + pluginInitializer := kubeadmission.New(c, nil, f, nil, nil, nil, nil, nil) pluginInitializer.Initialize(handler) err = admission.ValidateInitialization(handler) return handler, f, err diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/doc.go new file mode 100644 index 000000000..03728eda4 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package + +// Package policyconfig defines the configuration for the validating and mutating admission policy plugins. +package policyconfig // import "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig" diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/install/install.go new file mode 100644 index 000000000..e0925752c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/install/install.go @@ -0,0 +1,33 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package install installs the policyconfig API group, making it available as +// an option to all of the API encoding/decoding machinery. +package install + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig" + v1 "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig/v1" +) + +// Install registers the API group and adds types to a scheme +func Install(scheme *runtime.Scheme) { + utilruntime.Must(policyconfig.AddToScheme(scheme)) + utilruntime.Must(v1.AddToScheme(scheme)) + utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion)) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/register.go new file mode 100644 index 000000000..11976152c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/register.go @@ -0,0 +1,51 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policyconfig + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// GroupName is the group name use in this package +const GroupName = "apiserver.config.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal} + +// Kind takes an unqualified kind and returns a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &ValidatingAdmissionPolicyConfiguration{}, + &MutatingAdmissionPolicyConfiguration{}, + ) + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/types.go new file mode 100644 index 000000000..9af510122 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/types.go @@ -0,0 +1,63 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package policyconfig + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ValidatingAdmissionPolicyConfiguration provides configuration for the validating admission policy controller. +type ValidatingAdmissionPolicyConfiguration struct { + metav1.TypeMeta + + // StaticManifestsDir is the path to a directory containing static + // ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding + // resources to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string +} + +// GetStaticManifestsDir returns the static manifests directory path. +func (c *ValidatingAdmissionPolicyConfiguration) GetStaticManifestsDir() string { + return c.StaticManifestsDir +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// MutatingAdmissionPolicyConfiguration provides configuration for the mutating admission policy controller. +type MutatingAdmissionPolicyConfiguration struct { + metav1.TypeMeta + + // StaticManifestsDir is the path to a directory containing static + // MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding + // resources to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string +} + +// GetStaticManifestsDir returns the static manifests directory path. +func (c *MutatingAdmissionPolicyConfiguration) GetStaticManifestsDir() string { + return c.StaticManifestsDir +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/doc.go new file mode 100644 index 000000000..52837293d --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/doc.go @@ -0,0 +1,21 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig + +// Package v1 is the v1 version of the API. +package v1 // import "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig/v1" diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/register.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/register.go index 56489f780..f9b3d1176 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/register.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/register.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1alpha1 +package v1 import ( "k8s.io/apimachinery/pkg/runtime" @@ -25,7 +25,7 @@ import ( const GroupName = "apiserver.config.k8s.io" // SchemeGroupVersion is group version used to register these objects -var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} var ( // TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api. @@ -44,7 +44,8 @@ func init() { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, - &WebhookAdmission{}, + &ValidatingAdmissionPolicyConfiguration{}, + &MutatingAdmissionPolicyConfiguration{}, ) return nil } diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/types.go new file mode 100644 index 000000000..967b6ca3f --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/types.go @@ -0,0 +1,53 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ValidatingAdmissionPolicyConfiguration provides configuration for the validating admission policy controller. +type ValidatingAdmissionPolicyConfiguration struct { + metav1.TypeMeta `json:",inline"` + + // StaticManifestsDir is the path to a directory containing static + // ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding + // resources to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string `json:"staticManifestsDir,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// MutatingAdmissionPolicyConfiguration provides configuration for the mutating admission policy controller. +type MutatingAdmissionPolicyConfiguration struct { + metav1.TypeMeta `json:",inline"` + + // StaticManifestsDir is the path to a directory containing static + // MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding + // resources to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string `json:"staticManifestsDir,omitempty"` +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.conversion.go new file mode 100644 index 000000000..e65c4e52f --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.conversion.go @@ -0,0 +1,98 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1 + +import ( + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" + policyconfig "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*MutatingAdmissionPolicyConfiguration)(nil), (*policyconfig.MutatingAdmissionPolicyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_MutatingAdmissionPolicyConfiguration_To_policyconfig_MutatingAdmissionPolicyConfiguration(a.(*MutatingAdmissionPolicyConfiguration), b.(*policyconfig.MutatingAdmissionPolicyConfiguration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*policyconfig.MutatingAdmissionPolicyConfiguration)(nil), (*MutatingAdmissionPolicyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_policyconfig_MutatingAdmissionPolicyConfiguration_To_v1_MutatingAdmissionPolicyConfiguration(a.(*policyconfig.MutatingAdmissionPolicyConfiguration), b.(*MutatingAdmissionPolicyConfiguration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ValidatingAdmissionPolicyConfiguration)(nil), (*policyconfig.ValidatingAdmissionPolicyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_ValidatingAdmissionPolicyConfiguration_To_policyconfig_ValidatingAdmissionPolicyConfiguration(a.(*ValidatingAdmissionPolicyConfiguration), b.(*policyconfig.ValidatingAdmissionPolicyConfiguration), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*policyconfig.ValidatingAdmissionPolicyConfiguration)(nil), (*ValidatingAdmissionPolicyConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_policyconfig_ValidatingAdmissionPolicyConfiguration_To_v1_ValidatingAdmissionPolicyConfiguration(a.(*policyconfig.ValidatingAdmissionPolicyConfiguration), b.(*ValidatingAdmissionPolicyConfiguration), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1_MutatingAdmissionPolicyConfiguration_To_policyconfig_MutatingAdmissionPolicyConfiguration(in *MutatingAdmissionPolicyConfiguration, out *policyconfig.MutatingAdmissionPolicyConfiguration, s conversion.Scope) error { + out.StaticManifestsDir = in.StaticManifestsDir + return nil +} + +// Convert_v1_MutatingAdmissionPolicyConfiguration_To_policyconfig_MutatingAdmissionPolicyConfiguration is an autogenerated conversion function. +func Convert_v1_MutatingAdmissionPolicyConfiguration_To_policyconfig_MutatingAdmissionPolicyConfiguration(in *MutatingAdmissionPolicyConfiguration, out *policyconfig.MutatingAdmissionPolicyConfiguration, s conversion.Scope) error { + return autoConvert_v1_MutatingAdmissionPolicyConfiguration_To_policyconfig_MutatingAdmissionPolicyConfiguration(in, out, s) +} + +func autoConvert_policyconfig_MutatingAdmissionPolicyConfiguration_To_v1_MutatingAdmissionPolicyConfiguration(in *policyconfig.MutatingAdmissionPolicyConfiguration, out *MutatingAdmissionPolicyConfiguration, s conversion.Scope) error { + out.StaticManifestsDir = in.StaticManifestsDir + return nil +} + +// Convert_policyconfig_MutatingAdmissionPolicyConfiguration_To_v1_MutatingAdmissionPolicyConfiguration is an autogenerated conversion function. +func Convert_policyconfig_MutatingAdmissionPolicyConfiguration_To_v1_MutatingAdmissionPolicyConfiguration(in *policyconfig.MutatingAdmissionPolicyConfiguration, out *MutatingAdmissionPolicyConfiguration, s conversion.Scope) error { + return autoConvert_policyconfig_MutatingAdmissionPolicyConfiguration_To_v1_MutatingAdmissionPolicyConfiguration(in, out, s) +} + +func autoConvert_v1_ValidatingAdmissionPolicyConfiguration_To_policyconfig_ValidatingAdmissionPolicyConfiguration(in *ValidatingAdmissionPolicyConfiguration, out *policyconfig.ValidatingAdmissionPolicyConfiguration, s conversion.Scope) error { + out.StaticManifestsDir = in.StaticManifestsDir + return nil +} + +// Convert_v1_ValidatingAdmissionPolicyConfiguration_To_policyconfig_ValidatingAdmissionPolicyConfiguration is an autogenerated conversion function. +func Convert_v1_ValidatingAdmissionPolicyConfiguration_To_policyconfig_ValidatingAdmissionPolicyConfiguration(in *ValidatingAdmissionPolicyConfiguration, out *policyconfig.ValidatingAdmissionPolicyConfiguration, s conversion.Scope) error { + return autoConvert_v1_ValidatingAdmissionPolicyConfiguration_To_policyconfig_ValidatingAdmissionPolicyConfiguration(in, out, s) +} + +func autoConvert_policyconfig_ValidatingAdmissionPolicyConfiguration_To_v1_ValidatingAdmissionPolicyConfiguration(in *policyconfig.ValidatingAdmissionPolicyConfiguration, out *ValidatingAdmissionPolicyConfiguration, s conversion.Scope) error { + out.StaticManifestsDir = in.StaticManifestsDir + return nil +} + +// Convert_policyconfig_ValidatingAdmissionPolicyConfiguration_To_v1_ValidatingAdmissionPolicyConfiguration is an autogenerated conversion function. +func Convert_policyconfig_ValidatingAdmissionPolicyConfiguration_To_v1_ValidatingAdmissionPolicyConfiguration(in *policyconfig.ValidatingAdmissionPolicyConfiguration, out *ValidatingAdmissionPolicyConfiguration, s conversion.Scope) error { + return autoConvert_policyconfig_ValidatingAdmissionPolicyConfiguration_To_v1_ValidatingAdmissionPolicyConfiguration(in, out, s) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..f4382cd85 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/v1/zz_generated.deepcopy.go @@ -0,0 +1,76 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MutatingAdmissionPolicyConfiguration) DeepCopyInto(out *MutatingAdmissionPolicyConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyConfiguration. +func (in *MutatingAdmissionPolicyConfiguration) DeepCopy() *MutatingAdmissionPolicyConfiguration { + if in == nil { + return nil + } + out := new(MutatingAdmissionPolicyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MutatingAdmissionPolicyConfiguration) 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 *ValidatingAdmissionPolicyConfiguration) DeepCopyInto(out *ValidatingAdmissionPolicyConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingAdmissionPolicyConfiguration. +func (in *ValidatingAdmissionPolicyConfiguration) DeepCopy() *ValidatingAdmissionPolicyConfiguration { + if in == nil { + return nil + } + out := new(ValidatingAdmissionPolicyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValidatingAdmissionPolicyConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/zz_generated.deepcopy.go new file mode 100644 index 000000000..cfc56a61a --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/apis/policyconfig/zz_generated.deepcopy.go @@ -0,0 +1,76 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package policyconfig + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MutatingAdmissionPolicyConfiguration) DeepCopyInto(out *MutatingAdmissionPolicyConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MutatingAdmissionPolicyConfiguration. +func (in *MutatingAdmissionPolicyConfiguration) DeepCopy() *MutatingAdmissionPolicyConfiguration { + if in == nil { + return nil + } + out := new(MutatingAdmissionPolicyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MutatingAdmissionPolicyConfiguration) 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 *ValidatingAdmissionPolicyConfiguration) DeepCopyInto(out *ValidatingAdmissionPolicyConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValidatingAdmissionPolicyConfiguration. +func (in *ValidatingAdmissionPolicyConfiguration) DeepCopy() *ValidatingAdmissionPolicyConfiguration { + if in == nil { + return nil + } + out := new(ValidatingAdmissionPolicyConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ValidatingAdmissionPolicyConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config.go new file mode 100644 index 000000000..125b9d8b0 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config.go @@ -0,0 +1,97 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/admission/plugin/manifest" + "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig" + v1 "k8s.io/apiserver/pkg/admission/plugin/policy/config/apis/policyconfig/v1" +) + +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict) +) + +func init() { + utilruntime.Must(policyconfig.AddToScheme(scheme)) + utilruntime.Must(v1.AddToScheme(scheme)) +} + +// PolicyConfig holds the configuration loaded from the config file. +type PolicyConfig struct { + // StaticManifestsDir is the path to the directory containing static policy manifests. + StaticManifestsDir string +} + +// staticManifestsDirAccessor is implemented by config types that have a StaticManifestsDir field. +type staticManifestsDirAccessor interface { + runtime.Object + GetStaticManifestsDir() string +} + +// LoadValidatingConfig extracts the validating admission policy configuration from configFile. +func LoadValidatingConfig(configFile io.Reader) (PolicyConfig, error) { + return loadConfig(configFile, func(obj runtime.Object) bool { + _, ok := obj.(*policyconfig.ValidatingAdmissionPolicyConfiguration) + return ok + }) +} + +// LoadMutatingConfig extracts the mutating admission policy configuration from configFile. +func LoadMutatingConfig(configFile io.Reader) (PolicyConfig, error) { + return loadConfig(configFile, func(obj runtime.Object) bool { + _, ok := obj.(*policyconfig.MutatingAdmissionPolicyConfiguration) + return ok + }) +} + +func loadConfig(configFile io.Reader, isExpectedType func(runtime.Object) bool) (PolicyConfig, error) { + var cfg PolicyConfig + if configFile == nil { + return cfg, nil + } + + data, err := io.ReadAll(configFile) + if err != nil { + return cfg, err + } + decoder := codecs.UniversalDecoder() + decodedObj, err := runtime.Decode(decoder, data) + if err != nil { + return cfg, err + } + if !isExpectedType(decodedObj) { + return cfg, fmt.Errorf("unexpected type: %T", decodedObj) + } + config, ok := decodedObj.(staticManifestsDirAccessor) + if !ok { + return cfg, fmt.Errorf("type %T does not implement staticManifestsDirAccessor", decodedObj) + } + + if err := manifest.ValidateStaticManifestsDir(config.GetStaticManifestsDir()); err != nil { + return cfg, err + } + cfg.StaticManifestsDir = config.GetStaticManifestsDir() + return cfg, nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config_test.go new file mode 100644 index 000000000..a50cd7c37 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/config/config_test.go @@ -0,0 +1,245 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "bytes" + "os" + "path" + "strings" + "testing" + + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func TestLoadValidatingConfig(t *testing.T) { + validDir := t.TempDir() + notADirFile := path.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(notADirFile, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + testcases := []struct { + name string + input string + enableFeatureGate *bool + expectErr string + expectStaticManifestsDir string + }{ + { + name: "empty input", + input: "", + expectErr: `'Kind' is missing`, + }, + { + name: "unknown kind", + input: `{"kind":"Unknown","apiVersion":"v1"}`, + expectErr: `no kind "Unknown" is registered`, + }, + { + name: "valid config with staticManifestsDir", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: ` + validDir + ` +`, + enableFeatureGate: new(true), + expectStaticManifestsDir: validDir, + }, + { + name: "valid config with empty staticManifestsDir", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +`, + }, + { + name: "forbidden when feature gate disabled", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: /etc/kubernetes/policies +`, + enableFeatureGate: new(false), + expectErr: "staticManifestsDir: Forbidden", + }, + { + name: "invalid relative path", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: relative/path +`, + enableFeatureGate: new(true), + expectErr: "must be an absolute file path", + }, + { + name: "path does not exist", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: /nonexistent/path +`, + enableFeatureGate: new(true), + expectErr: "unable to read", + }, + { + name: "path is not a directory", + input: ` +kind: ValidatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: ` + notADirFile + ` +`, + enableFeatureGate: new(true), + expectErr: "must be a directory", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + if tc.enableFeatureGate != nil { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ManifestBasedAdmissionControlConfig, *tc.enableFeatureGate) + } + cfg, err := LoadValidatingConfig(bytes.NewBufferString(tc.input)) + if len(tc.expectErr) > 0 { + if err == nil { + t.Fatal("expected err, got none") + } + if !strings.Contains(err.Error(), tc.expectErr) { + t.Fatalf("expected err containing %q, got %v", tc.expectErr, err) + } + return + } + if err != nil { + t.Fatal(err) + } + if cfg.StaticManifestsDir != tc.expectStaticManifestsDir { + t.Fatalf("expected StaticManifestsDir %q, got %q", tc.expectStaticManifestsDir, cfg.StaticManifestsDir) + } + }) + } +} + +func TestLoadMutatingConfig(t *testing.T) { + validDir := t.TempDir() + notADirFile := path.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(notADirFile, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + testcases := []struct { + name string + input string + enableFeatureGate *bool + expectErr string + expectStaticManifestsDir string + }{ + { + name: "empty input", + input: "", + expectErr: `'Kind' is missing`, + }, + { + name: "unknown kind", + input: `{"kind":"Unknown","apiVersion":"v1"}`, + expectErr: `no kind "Unknown" is registered`, + }, + { + name: "valid config with staticManifestsDir", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: ` + validDir + ` +`, + enableFeatureGate: new(true), + expectStaticManifestsDir: validDir, + }, + { + name: "valid config with empty staticManifestsDir", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +`, + }, + { + name: "forbidden when feature gate disabled", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: /etc/kubernetes/policies +`, + enableFeatureGate: new(false), + expectErr: "staticManifestsDir: Forbidden", + }, + { + name: "invalid relative path", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: relative/path +`, + enableFeatureGate: new(true), + expectErr: "must be an absolute file path", + }, + { + name: "path does not exist", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: /nonexistent/path +`, + enableFeatureGate: new(true), + expectErr: "unable to read", + }, + { + name: "path is not a directory", + input: ` +kind: MutatingAdmissionPolicyConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: ` + notADirFile + ` +`, + enableFeatureGate: new(true), + expectErr: "must be a directory", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + if tc.enableFeatureGate != nil { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ManifestBasedAdmissionControlConfig, *tc.enableFeatureGate) + } + cfg, err := LoadMutatingConfig(bytes.NewBufferString(tc.input)) + if len(tc.expectErr) > 0 { + if err == nil { + t.Fatal("expected err, got none") + } + if !strings.Contains(err.Error(), tc.expectErr) { + t.Fatalf("expected err containing %q, got %v", tc.expectErr, err) + } + return + } + if err != nil { + t.Fatal(err) + } + if cfg.StaticManifestsDir != tc.expectStaticManifestsDir { + t.Fatalf("expected StaticManifestsDir %q, got %q", tc.expectStaticManifestsDir, cfg.StaticManifestsDir) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/accessor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/accessor.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/accessor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/accessor.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source.go new file mode 100644 index 000000000..f991674e5 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source.go @@ -0,0 +1,119 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package generic + +import ( + "context" + "sync" +) + +// HookSource provides hooks and sync status. This is the minimal interface +// needed for static sources that don't have their own Run lifecycle. +type HookSource[H Hook] interface { + Hooks() []H + HasSynced() bool +} + +// compositePolicySource combines multiple policy sources into a single source. +// Static (manifest-based) policies are returned before API-based policies. +type compositePolicySource[H Hook] struct { + staticSource HookSource[H] + apiSource Source[H] + + mu sync.RWMutex + lastStatic []H + lastAPI []H + lastCombined []H +} + +var _ Source[Hook] = &compositePolicySource[Hook]{} + +// NewCompositePolicySource creates a policy source that combines static and API-based sources. +// Static policies are evaluated first, followed by API-based policies. +// If staticSource is nil, only apiSource policies are returned. +func NewCompositePolicySource[H Hook](staticSource HookSource[H], apiSource Source[H]) Source[H] { + if staticSource == nil { + return apiSource + } + return &compositePolicySource[H]{ + staticSource: staticSource, + apiSource: apiSource, + } +} + +// Hooks returns all policy hooks from both sources. +// Static policies come first, followed by API-based policies. +// The combined slice is cached and reused when the underlying slices haven't changed. +func (c *compositePolicySource[H]) Hooks() []H { + var staticHooks, apiHooks []H + + // Static policies first (platform policies take precedence) + if c.staticSource != nil { + staticHooks = c.staticSource.Hooks() + } + + // Then API-based policies + if c.apiSource != nil { + apiHooks = c.apiSource.Hooks() + } + + c.mu.RLock() + if slicesAreEqual(staticHooks, c.lastStatic) && slicesAreEqual(apiHooks, c.lastAPI) { + combined := c.lastCombined + c.mu.RUnlock() + return combined + } + c.mu.RUnlock() + + c.mu.Lock() + defer c.mu.Unlock() + // Re-check under write lock. + if slicesAreEqual(staticHooks, c.lastStatic) && slicesAreEqual(apiHooks, c.lastAPI) { + return c.lastCombined + } + + combined := make([]H, 0, len(staticHooks)+len(apiHooks)) + combined = append(combined, staticHooks...) + combined = append(combined, apiHooks...) + + c.lastStatic = staticHooks + c.lastAPI = apiHooks + c.lastCombined = combined + return combined +} + +// slicesAreEqual reports whether two slices share the same backing array and length. +func slicesAreEqual[T any](a, b []T) bool { + return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0]) +} + +// Run starts the API-based source. The static source is started separately +// by the plugin via its own goroutine (file watcher), so this method only +// needs to handle the API source lifecycle. +func (c *compositePolicySource[H]) Run(ctx context.Context) error { + if c.apiSource != nil { + return c.apiSource.Run(ctx) + } + return nil +} + +// HasSynced returns true only when both sources have synced. +func (c *compositePolicySource[H]) HasSynced() bool { + staticSynced := c.staticSource == nil || c.staticSource.HasSynced() + apiSynced := c.apiSource == nil || c.apiSource.HasSynced() + return staticSynced && apiSynced +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source_test.go new file mode 100644 index 000000000..7dc547d7a --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/composite_policy_source_test.go @@ -0,0 +1,186 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package generic + +import ( + "context" + "testing" +) + +// testHook is a minimal Hook implementation for testing. +type testHook struct { + name string +} + +// mockPolicySource implements Source[testHook] for testing. +type mockPolicySource struct { + hooks []testHook + hasSynced bool +} + +func (m *mockPolicySource) Hooks() []testHook { + return m.hooks +} + +func (m *mockPolicySource) Run(_ context.Context) error { + return nil +} + +func (m *mockPolicySource) HasSynced() bool { + return m.hasSynced +} + +var _ Source[testHook] = &mockPolicySource{} + +func TestCompositePolicySource_Hooks(t *testing.T) { + staticHook := testHook{name: "static-1"} + apiHook := testHook{name: "api-1"} + + tests := []struct { + name string + staticSource Source[testHook] + apiSource Source[testHook] + wantNames []string + }{ + { + name: "only static source", + staticSource: &mockPolicySource{hooks: []testHook{staticHook}, hasSynced: true}, + apiSource: &mockPolicySource{hooks: nil, hasSynced: true}, + wantNames: []string{"static-1"}, + }, + { + name: "only api source", + staticSource: &mockPolicySource{hooks: nil, hasSynced: true}, + apiSource: &mockPolicySource{hooks: []testHook{apiHook}, hasSynced: true}, + wantNames: []string{"api-1"}, + }, + { + name: "both sources - static first", + staticSource: &mockPolicySource{hooks: []testHook{staticHook}, hasSynced: true}, + apiSource: &mockPolicySource{hooks: []testHook{apiHook}, hasSynced: true}, + wantNames: []string{"static-1", "api-1"}, + }, + { + name: "nil static source", + staticSource: nil, + apiSource: &mockPolicySource{hooks: []testHook{apiHook}, hasSynced: true}, + wantNames: []string{"api-1"}, + }, + { + name: "both empty", + staticSource: &mockPolicySource{hooks: nil, hasSynced: true}, + apiSource: &mockPolicySource{hooks: nil, hasSynced: true}, + wantNames: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := NewCompositePolicySource[testHook](tt.staticSource, tt.apiSource) + hooks := source.Hooks() + + if len(hooks) != len(tt.wantNames) { + t.Errorf("Hooks() returned %d hooks, want %d", len(hooks), len(tt.wantNames)) + return + } + + for i, h := range hooks { + if h.name != tt.wantNames[i] { + t.Errorf("Hooks()[%d].name = %s, want %s", i, h.name, tt.wantNames[i]) + } + } + }) + } +} + +func TestCompositePolicySource_Caching(t *testing.T) { + staticSource := &mockPolicySource{hooks: []testHook{{name: "static-1"}}, hasSynced: true} + apiSource := &mockPolicySource{hooks: []testHook{{name: "api-1"}}, hasSynced: true} + + source := NewCompositePolicySource[testHook](staticSource, apiSource) + + // First call should create the combined slice. + hooks1 := source.Hooks() + if len(hooks1) != 2 { + t.Fatalf("Expected 2 hooks, got %d", len(hooks1)) + } + + // Second call with same underlying slices should return the cached combined slice. + hooks2 := source.Hooks() + if &hooks1[0] != &hooks2[0] { + t.Error("Expected cached slice to be returned when underlying slices haven't changed") + } + + // Changing the underlying slice should produce a new combined slice. + staticSource.hooks = []testHook{{name: "static-2"}} + hooks3 := source.Hooks() + if hooks3[0].name != "static-2" { + t.Errorf("Expected first hook to be static-2 after update, got %s", hooks3[0].name) + } + if &hooks2[0] == &hooks3[0] { + t.Error("Expected new slice after underlying source changed") + } +} + +func TestCompositePolicySource_HasSynced(t *testing.T) { + tests := []struct { + name string + staticSource Source[testHook] + apiSource Source[testHook] + want bool + }{ + { + name: "both synced", + staticSource: &mockPolicySource{hasSynced: true}, + apiSource: &mockPolicySource{hasSynced: true}, + want: true, + }, + { + name: "static not synced", + staticSource: &mockPolicySource{hasSynced: false}, + apiSource: &mockPolicySource{hasSynced: true}, + want: false, + }, + { + name: "api not synced", + staticSource: &mockPolicySource{hasSynced: true}, + apiSource: &mockPolicySource{hasSynced: false}, + want: false, + }, + { + name: "neither synced", + staticSource: &mockPolicySource{hasSynced: false}, + apiSource: &mockPolicySource{hasSynced: false}, + want: false, + }, + { + name: "nil static source, api synced", + staticSource: nil, + apiSource: &mockPolicySource{hasSynced: true}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := NewCompositePolicySource[testHook](tt.staticSource, tt.apiSource) + if got := source.HasSynced(); got != tt.want { + t.Errorf("HasSynced() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/plugin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/plugin.go new file mode 100644 index 000000000..20771583c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/plugin.go @@ -0,0 +1,347 @@ +/* +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 generic + +import ( + "context" + "errors" + "fmt" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/admission/plugin/policy/matching" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" +) + +// H is the Hook type generated by the source and consumed by the dispatcher. +// !TODO: Just pass in a Plugin[H] with accessors to all this information +type apiSourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H] +type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H] + +// ReloadableSource extends HookSource with a method to run a reload loop +// that watches for configuration changes and blocks until the context is canceled. +type ReloadableSource[H any] interface { + HookSource[H] + // RunReloadLoop watches for configuration changes and reloads when detected. + // It blocks until ctx is canceled. + RunReloadLoop(ctx context.Context) +} + +// StaticSourceFactory creates a static policy source from a manifest directory. +// The returned Source should have LoadInitial() already called. +type StaticSourceFactory[H any] func(manifestsDir string) (ReloadableSource[H], error) + +// admissionResources is the list of resources related to CEL-based admission +// features. +var admissionResources = []schema.GroupResource{ + {Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"}, + {Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"}, + {Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"}, + {Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"}, +} + +// AdmissionPolicyManager is an abstract admission plugin with all the +// infrastructure to define Admit or Validate on-top. +type Plugin[H any] struct { + *admission.Handler + + apiSourceFactory apiSourceFactory[H] + dispatcherFactory dispatcherFactory[H] + staticSourceFactory StaticSourceFactory[H] + + source Source[H] + dispatcher Dispatcher[H] + matcher *matching.Matcher + + // staticSource holds a reference to only the static (manifest-based) policy source. + // This is used in Dispatch to run static hooks for admission config resources + // (e.g., VAP/MAP/VAPB/MAPB), allowing static policies to protect those resources + // without the circular dependency concern that applies to API-based policies. + staticSource HookSource[H] + + // staticManifestsDir is the path to a directory containing static policy manifests. + // When set and the feature gate is enabled, policies are also loaded from this directory. + staticManifestsDir string + + // apiServerID is the identity of this API server instance, used for metrics labeling. + apiServerID string + + informerFactory informers.SharedInformerFactory + client kubernetes.Interface + restMapper meta.RESTMapper + dynamicClient dynamic.Interface + // admissionConfigResources are admission configuration resources (VAP/MAP/VAPB/MAPB). + // API-based policies skip these to prevent circular dependencies, but static policies + // are safe to evaluate on them. + admissionConfigResources sets.Set[schema.GroupResource] + // excludedResources are non-persisted resources (auth/authz reviews) that must be + // excluded from ALL policy evaluation, including static policies. + excludedResources sets.Set[schema.GroupResource] + stopCh <-chan struct{} + authorizer authorizer.Authorizer + enabled bool +} + +var ( + _ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{} + _ initializer.WantsExternalKubeClientSet = &Plugin[any]{} + _ initializer.WantsRESTMapper = &Plugin[any]{} + _ initializer.WantsDynamicClient = &Plugin[any]{} + _ initializer.WantsDrainedNotification = &Plugin[any]{} + _ initializer.WantsAPIServerID = &Plugin[any]{} + _ initializer.WantsAuthorizer = &Plugin[any]{} + _ initializer.WantsExcludedAdmissionResources = &Plugin[any]{} + _ admission.InitializationValidator = &Plugin[any]{} +) + +func NewPlugin[H any]( + handler *admission.Handler, + apiSourceFactory apiSourceFactory[H], + dispatcherFactory dispatcherFactory[H], +) *Plugin[H] { + return &Plugin[H]{ + Handler: handler, + apiSourceFactory: apiSourceFactory, + dispatcherFactory: dispatcherFactory, + + // admission config resources are skipped by API-based policies (circular dep) + // but evaluated by static policies + admissionConfigResources: sets.New(admissionResources...), + excludedResources: sets.New[schema.GroupResource](), + } +} + +func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { + c.informerFactory = f +} + +func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) { + c.client = client +} + +func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) { + c.restMapper = mapper +} + +func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) { + c.dynamicClient = client +} + +func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) { + c.stopCh = stopCh +} + +func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) { + c.authorizer = authorizer +} + +func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) { + c.matcher = matcher +} + +func (c *Plugin[H]) SetEnabled(enabled bool) { + c.enabled = enabled +} + +func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) { + c.excludedResources.Insert(excludedResources...) +} + +// SetStaticManifestsDir sets the directory containing static policy manifests. +func (c *Plugin[H]) SetStaticManifestsDir(dir string) { + c.staticManifestsDir = dir +} + +// GetStaticManifestsDir returns the directory containing static policy manifests. +func (c *Plugin[H]) GetStaticManifestsDir() string { + return c.staticManifestsDir +} + +// SetStaticSourceFactory sets the factory for creating static policy sources. +// This should be called before ValidateInitialization. +func (c *Plugin[H]) SetStaticSourceFactory(factory StaticSourceFactory[H]) { + c.staticSourceFactory = factory +} + +// SetAPIServerID implements the WantsAPIServerID interface. +// The API server ID is used for metrics labeling and must be set before +// ValidateInitialization is called. +func (c *Plugin[H]) SetAPIServerID(id string) { + c.apiServerID = id +} + +// GetAPIServerID returns the stored API server ID. +func (c *Plugin[H]) GetAPIServerID() string { + return c.apiServerID +} + +// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller +func (c *Plugin[H]) ValidateInitialization() error { + // By default enabled is set to false. It is up to types which embed this + // struct to set it to true (if feature gate is enabled, or other conditions) + if !c.enabled { + return nil + } + if c.Handler == nil { + return errors.New("missing handler") + } + if c.informerFactory == nil { + return errors.New("missing informer factory") + } + if c.client == nil { + return errors.New("missing kubernetes client") + } + if c.restMapper == nil { + return errors.New("missing rest mapper") + } + if c.dynamicClient == nil { + return errors.New("missing dynamic client") + } + if c.stopCh == nil { + return errors.New("missing stop channel") + } + if c.authorizer == nil { + return errors.New("missing authorizer") + } + + // Guard: if static manifests dir is set but feature gate is off, return an error + if len(c.staticManifestsDir) > 0 && !utilfeature.DefaultFeatureGate.Enabled(features.ManifestBasedAdmissionControlConfig) { + return fmt.Errorf("static policy manifests dir %q configured but %s feature gate is not enabled", c.staticManifestsDir, features.ManifestBasedAdmissionControlConfig) + } + + namespaceInformer := c.informerFactory.Core().V1().Namespaces() + + // Construct matcher once, avoiding overwriting if already set. + if c.matcher == nil { + c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client) + } + if err := c.matcher.ValidateInitialization(); err != nil { + return err + } + + // Create a shared context tied to server shutdown for all background goroutines. + pluginContext, pluginContextCancel := context.WithCancel(context.Background()) + go func() { + defer pluginContextCancel() + <-c.stopCh + }() + + // Construct source once, avoiding overwriting if already set. + if c.source == nil { + apiSource := c.apiSourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper) + + if len(c.staticManifestsDir) > 0 { + if c.staticSourceFactory == nil { + return fmt.Errorf("static policy manifests configured in %q but no static source factory is set", c.staticManifestsDir) + } + staticSource, err := c.staticSourceFactory(c.staticManifestsDir) + if err != nil { + return fmt.Errorf("failed to load static policy manifests from %q: %w", c.staticManifestsDir, err) + } + // Use composite source that combines static + API sources + c.source = NewCompositePolicySource(staticSource, apiSource) + // Keep a reference to the static source so Dispatch can run static hooks + // for resources that are normally excluded from policy evaluation. + c.staticSource = staticSource + // Start the file watcher in a background goroutine + go staticSource.RunReloadLoop(pluginContext) + } else { + // Use only API-based source + c.source = apiSource + } + + go func() { + err := c.source.Run(pluginContext) + if err != nil && !errors.Is(err, context.Canceled) { + utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err)) + } + }() + + c.SetReadyFunc(func() bool { + return namespaceInformer.Informer().HasSynced() && c.source.HasSynced() + }) + } + + // Construct dispatcher once, avoiding overwriting if already set. + if c.dispatcher == nil { + c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client) + + err := c.dispatcher.Start(pluginContext) + if err != nil && !errors.Is(err, context.Canceled) { + utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err)) + } + } + + return nil +} + +func (c *Plugin[H]) Dispatch( + ctx context.Context, + a admission.Attributes, + o admission.ObjectInterfaces, +) (err error) { + if !c.enabled { + return nil + } + + gr := a.GetResource().GroupResource() + + if c.isExcludedFromAllHooks(gr) { + // Non-persisted resources (auth/authz reviews) are excluded from all policy + // evaluation to avoid breaking the cluster. + return nil + } else if c.isExcludedFromAPIHooks(gr) { + // Admission config resources (VAP/MAP/VAPB/MAPB) are excluded from API-based + // policies to prevent circular dependencies. However, static (manifest-based) + // policies are safe to evaluate since they don't have self-referential concerns. + if c.staticSource != nil { + if !c.staticSource.HasSynced() { + return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) + } + return c.dispatcher.Dispatch(ctx, a, o, c.staticSource.Hooks()) + } + return nil + } + + if !c.WaitForReady() { + return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) + } + + return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks()) +} + +// isExcludedFromAllHooks returns true for non-persisted resources (auth/authz reviews) +// that must not be intercepted by any policy hook. +func (c *Plugin[H]) isExcludedFromAllHooks(gr schema.GroupResource) bool { + return c.excludedResources.Has(gr) +} + +// isExcludedFromAPIHooks returns true for admission config resources (VAP/MAP/VAPB/MAPB) +// that are excluded from API-based hooks but may be evaluated by static hooks. +func (c *Plugin[H]) isExcludedFromAPIHooks(gr schema.GroupResource) bool { + return c.admissionConfigResources.Has(gr) +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_dispatcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_dispatcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_dispatcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_dispatcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_matcher.go similarity index 81% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_matcher.go index d243b0710..782b6f1f4 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_matcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_matcher.go @@ -17,6 +17,7 @@ limitations under the License. package generic import ( + "context" "fmt" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" @@ -41,10 +42,12 @@ type PolicyMatcher interface { BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding BindingAccessor) (bool, error) // GetNamespace retrieves the Namespace resource by the given name. The name may be empty, in which case - // GetNamespace must return nil, nil - GetNamespace(name string) (*corev1.Namespace, error) + // GetNamespace must return nil, NotFound + GetNamespace(ctx context.Context, name string) (*corev1.Namespace, error) } +var errNilSelector = "a nil %s selector was passed, please ensure selectors are initialized properly" + type matcher struct { Matcher *matching.Matcher } @@ -66,6 +69,13 @@ func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectIn if constraints == nil { return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("policy contained no match constraints, a required field") } + if constraints.NamespaceSelector == nil { + return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf(errNilSelector, "namespace") + } + if constraints.ObjectSelector == nil { + return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf(errNilSelector, "object") + } + criteria := matchCriteria{constraints: constraints} return c.Matcher.Matches(a, o, &criteria) } @@ -76,14 +86,20 @@ func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInter if matchResources == nil { return true, nil } + if matchResources.NamespaceSelector == nil { + return false, fmt.Errorf(errNilSelector, "namespace") + } + if matchResources.ObjectSelector == nil { + return false, fmt.Errorf(errNilSelector, "object") + } criteria := matchCriteria{constraints: matchResources} isMatch, _, _, err := c.Matcher.Matches(a, o, &criteria) return isMatch, err } -func (c *matcher) GetNamespace(name string) (*corev1.Namespace, error) { - return c.Matcher.GetNamespace(name) +func (c *matcher) GetNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { + return c.Matcher.GetNamespace(ctx, name) } var _ matching.MatchCriteria = &matchCriteria{} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_source.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_source.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_source.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_source.go index ca6cdc884..09e4895e9 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_source.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_source.go @@ -47,6 +47,7 @@ import ( const policyRefreshIntervalDefault = 1 * time.Second var policyRefreshInterval = policyRefreshIntervalDefault +var policyRefreshIntervalLock sync.Mutex type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct { ctx context.Context @@ -132,8 +133,12 @@ func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator]( // SetPolicyRefreshIntervalForTests allows the refresh interval to be overridden during tests. // This should only be called from tests. func SetPolicyRefreshIntervalForTests(interval time.Duration) func() { + policyRefreshIntervalLock.Lock() + defer policyRefreshIntervalLock.Unlock() policyRefreshInterval = interval return func() { + policyRefreshIntervalLock.Lock() + defer policyRefreshIntervalLock.Unlock() policyRefreshInterval = policyRefreshIntervalDefault } } @@ -145,7 +150,7 @@ func (s *policySource[P, B, E]) Run(ctx context.Context) error { // Wait for initial cache sync of policies and informers before reconciling // any - if !cache.WaitForNamedCacheSync(fmt.Sprintf("%T", s), ctx.Done(), s.UpstreamHasSynced) { + if !cache.WaitForNamedCacheSyncWithContext(ctx, s.UpstreamHasSynced) { err := ctx.Err() if err == nil { err = fmt.Errorf("initial cache sync for %T failed", s) @@ -194,7 +199,10 @@ func (s *policySource[P, B, E]) Run(ctx context.Context) error { // and needs to be recompiled go func() { // Loop every 1 second until context is cancelled, refreshing policies - wait.Until(s.refreshPolicies, policyRefreshInterval, ctx.Done()) + policyRefreshIntervalLock.Lock() + interval := policyRefreshInterval + policyRefreshIntervalLock.Unlock() + wait.Until(s.refreshPolicies, interval, ctx.Done()) }() <-ctx.Done() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_source_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_source_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_source_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_source_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_test_context.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_test_context.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_test_context.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_test_context.go index d9a737a60..5a9890286 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/generic/policy_test_context.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/generic/policy_test_context.go @@ -45,6 +45,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/util/compatibility" ) // Logger allows t.Testing and b.Testing to be passed to PolicyTestContext @@ -158,27 +159,27 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator]( // Make an informer for our policies and bindings policyInformer := cache.NewSharedIndexInformer( - &cache.ListWatch{ + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return policiesAndBindingsTracker.List(fakePolicyGVR, fakePolicyGVK, "") }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - return policiesAndBindingsTracker.Watch(fakePolicyGVR, "") + return policiesAndBindingsTracker.Watch(fakePolicyGVR, "", options) }, - }, + }, policiesAndBindingsTracker), Pexample, 30*time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, ) bindingInformer := cache.NewSharedIndexInformer( - &cache.ListWatch{ + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return policiesAndBindingsTracker.List(fakeBindingGVR, fakeBindingGVK, "") }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - return policiesAndBindingsTracker.Watch(fakeBindingGVR, "") + return policiesAndBindingsTracker.Watch(fakeBindingGVR, "", options) }, - }, + }, policiesAndBindingsTracker), Bexample, 30*time.Second, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, @@ -203,6 +204,7 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator]( plugin.SetEnabled(true) featureGate := featuregate.NewFeatureGate() + effectiveVersion := compatibility.DefaultBuildEffectiveVersion() testContext, testCancel := context.WithCancel(context.Background()) genericInitializer := initializer.New( nativeClient, @@ -210,6 +212,7 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator]( fakeInformerFactory, fakeAuthorizer{}, featureGate, + effectiveVersion, testContext.Done(), fakeRestMapper, ) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller.go index f08e1cc55..65ffee4e2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller.go @@ -21,7 +21,6 @@ import ( "errors" "fmt" "sync" - "sync/atomic" "time" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -48,10 +47,7 @@ type controller[T runtime.Object] struct { options ControllerOptions - // must hold a func() bool or nil - notificationsDelivered atomic.Value - - hasProcessed synctrack.AsyncTracker[string] + hasProcessed *synctrack.AsyncTracker[string] } type ControllerOptions struct { @@ -77,17 +73,11 @@ func NewController[T runtime.Object]( } c := &controller[T]{ - options: options, - informer: informer, - reconciler: reconciler, - queue: nil, - } - c.hasProcessed.UpstreamHasSynced = func() bool { - f := c.notificationsDelivered.Load() - if f == nil { - return false - } - return f.(func() bool)() + options: options, + informer: informer, + reconciler: reconciler, + queue: nil, + hasProcessed: synctrack.NewAsyncTracker[string](options.Name), } return c } @@ -159,12 +149,9 @@ func (c *controller[T]) Run(ctx context.Context) error { return err } - c.notificationsDelivered.Store(registration.HasSynced) - // Make sure event handler is removed from informer in case return early from // an error defer func() { - c.notificationsDelivered.Store(func() bool { return false }) // Remove event handler and Handle Error here. Error should only be raised // for improper usage of event handler API. if err := c.informer.RemoveEventHandler(registration); err != nil { @@ -174,7 +161,12 @@ func (c *controller[T]) Run(ctx context.Context) error { // Wait for initial cache list to complete before beginning to reconcile // objects. - if !cache.WaitForNamedCacheSync(c.options.Name, ctx.Done(), c.informer.HasSynced) { + if !cache.WaitFor(ctx, "caches", c.informer.HasSyncedChecker(), registration.HasSyncedChecker()) { + // TODO: should cache.WaitFor return an error? + // ctx.Err() or context.Cause(ctx)? + // Either of them would make dead code like the "if err == nil" + // below more obvious. + // ctx cancelled during cache sync. return early err := ctx.Err() if err == nil { @@ -184,6 +176,10 @@ func (c *controller[T]) Run(ctx context.Context) error { return err } + // c.informer *and* our handler have synced, which implies that our AddFunc(= enqueue) + // and thus c.hasProcessed.Start have been called for the initial list => upstream is done. + c.hasProcessed.UpstreamHasSynced() + waitGroup := sync.WaitGroup{} for i := uint(0); i < c.options.Workers; i++ { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller_test.go index 994239104..efe7a5e9d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/controller_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/controller_test.go @@ -38,7 +38,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" clienttesting "k8s.io/client-go/testing" @@ -113,14 +112,14 @@ func setupTest(ctx context.Context, customReconciler func(string, string, runtim // Set up fake informers that return instances of mock Policy definitoins // and mock policy bindings - informer = &testInformer{SharedIndexInformer: cache.NewSharedIndexInformer(&cache.ListWatch{ + informer = &testInformer{SharedIndexInformer: cache.NewSharedIndexInformer(cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return tracker.List(fakeGVR, fakeGVK, "") }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - return tracker.Watch(fakeGVR, "") + return tracker.Watch(fakeGVR, "", options) }, - }, &unstructured.Unstructured{}, 30*time.Second, nil)} + }, tracker), &unstructured.Unstructured{}, 30*time.Second, nil)} reconciler := func(namespace, name string, newObj *unstructured.Unstructured) error { var err error @@ -371,7 +370,7 @@ func TestIgnoredUpdate(t *testing.T) { // Shows that an object which fails reconciliation will retry func TestReconcileRetry(t *testing.T) { - testContext, testCancel := context.WithTimeout(context.Background(), 2*time.Second) + testContext, testCancel := context.WithTimeout(context.Background(), wait.ForeverTestTimeout) defer testCancel() calls := atomic.Uint64{} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/informer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/informer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/informer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/informer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/lister.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/lister.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/internal/generic/lister.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/internal/generic/lister.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader.go new file mode 100644 index 000000000..430da1e3c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader.go @@ -0,0 +1,227 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package loader provides generic functionality to load policy and binding +// configurations from manifest files. It handles file reading, YAML/JSON +// decoding, manifest name validation (suffix + uniqueness), policy-specific +// manifest constraints, and binding-reference checking. Type-specific +// defaulting and validation (e.g., scheme-based defaulting via internal +// types) are injected by callers through the AcceptObjectFunc callbacks. +package loader + +import ( + "errors" + "fmt" + "sort" + "strings" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/admission/plugin/manifest" +) + +// ErrUnrecognizedType is returned by AcceptObjectFunc when the object type is +// not handled. This allows callers with multiple accept functions to try the next one. +var ErrUnrecognizedType = fmt.Errorf("unrecognized resource type") + +// AcceptObjectFunc extracts typed items from a decoded runtime.Object, applying +// defaulting and validation. Returns ErrUnrecognizedType if the object type is +// not handled by this function. +type AcceptObjectFunc[T metav1.Object] func(obj runtime.Object) ([]T, error) + +// LoadPolicyManifests is a generic helper that loads policy and binding manifests +// from a directory. It handles file I/O, decoding, v1.List unwrapping, manifest +// name validation, policy-specific manifest constraints, binding-reference checking, +// and deterministic sorting by name. The decoder should be created by the caller +// with whatever scheme is appropriate (e.g., with full internal type install for +// kube-apiserver, or a minimal v1-only scheme for other consumers). +func LoadPolicyManifests[P, B metav1.Object]( + dir string, + decoder runtime.Decoder, + acceptPolicy AcceptObjectFunc[P], + acceptBinding AcceptObjectFunc[B], + getBindingPolicyName func(B) string, +) ([]P, []B, string, error) { + fileDocs, hash, err := manifest.LoadFiles(dir) + if err != nil { + return nil, nil, "", err + } + + policies := make([]P, 0) + bindings := make([]B, 0) + seenPolicyNames := map[string]string{} + seenBindingNames := map[string]string{} + + for _, fd := range fileDocs { + obj, _, err := decoder.Decode(fd.Doc, nil, nil) + if err != nil { + return nil, nil, "", fmt.Errorf("error loading %s: %w", fd.FilePath, err) + } + + newPolicies, newBindings, err := acceptFileObject(obj, fd.FilePath, seenPolicyNames, seenBindingNames, decoder, acceptPolicy, acceptBinding) + if err != nil { + return nil, nil, "", fmt.Errorf("error loading %s: %w", fd.FilePath, err) + } + policies = append(policies, newPolicies...) + bindings = append(bindings, newBindings...) + } + + if err := validateBindingReferences(policies, bindings, getBindingPolicyName); err != nil { + return nil, nil, "", err + } + + sort.Slice(policies, func(i, j int) bool { + return policies[i].GetName() < policies[j].GetName() + }) + sort.Slice(bindings, func(i, j int) bool { + return bindings[i].GetName() < bindings[j].GetName() + }) + + return policies, bindings, hash, nil +} + +// acceptFileObject handles type dispatch for a decoded object, including +// generic v1.List unwrapping, manifest name validation, policy-specific +// manifest constraint validation, and the default unsupported-type error. +func acceptFileObject[P, B metav1.Object]( + obj runtime.Object, + filePath string, + seenPolicyNames, seenBindingNames map[string]string, + decoder runtime.Decoder, + acceptPolicy AcceptObjectFunc[P], + acceptBinding AcceptObjectFunc[B], +) ([]P, []B, error) { + // Handle generic v1.List by recursing into each item + if list, ok := obj.(*metav1.List); ok { + var allPolicies []P + var allBindings []B + for _, rawItem := range list.Items { + itemObj, _, err := decoder.Decode(rawItem.Raw, nil, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode list item: %w", err) + } + p, b, err := acceptFileObject(itemObj, filePath, seenPolicyNames, seenBindingNames, decoder, acceptPolicy, acceptBinding) + if err != nil { + return nil, nil, err + } + allPolicies = append(allPolicies, p...) + allBindings = append(allBindings, b...) + } + return allPolicies, allBindings, nil + } + + // Try policy + policies, pErr := acceptPolicy(obj) + if pErr != nil && !errors.Is(pErr, ErrUnrecognizedType) { + return nil, nil, pErr + } + if pErr == nil { + for _, item := range policies { + if err := validateAcceptedItem(item, filePath, seenPolicyNames); err != nil { + return nil, nil, err + } + } + return policies, nil, nil + } + + // Try binding + bindings, bErr := acceptBinding(obj) + if bErr != nil && !errors.Is(bErr, ErrUnrecognizedType) { + return nil, nil, bErr + } + if bErr == nil { + for _, item := range bindings { + if err := validateAcceptedItem(item, filePath, seenBindingNames); err != nil { + return nil, nil, err + } + } + return nil, bindings, nil + } + + // Neither recognized it + return nil, nil, fmt.Errorf("unsupported resource type %T", obj) +} + +// validateAcceptedItem runs manifest name validation and policy-specific +// manifest constraint validation. +func validateAcceptedItem[T metav1.Object](item T, filePath string, seenNames map[string]string) error { + name := item.GetName() + if err := manifest.ValidateManifestName(name, filePath, seenNames); err != nil { + return err + } + obj, ok := any(item).(runtime.Object) + if !ok { + return fmt.Errorf("type %T does not implement runtime.Object", item) + } + return validatePolicyManifestConstraints(obj, name) +} + +// validatePolicyManifestConstraints validates manifest-specific constraints +// for policy and binding types (paramKind, paramRef, policyName suffix). +func validatePolicyManifestConstraints(obj runtime.Object, name string) error { + switch c := obj.(type) { + case *admissionregistrationv1.ValidatingAdmissionPolicy: + if c.Spec.ParamKind != nil { + return fmt.Errorf("ValidatingAdmissionPolicy %q: spec.paramKind is not supported for static manifests", name) + } + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + if err := validateBindingConstraints(name, c.Spec.PolicyName, c.Spec.ParamRef != nil); err != nil { + return err + } + case *admissionregistrationv1.MutatingAdmissionPolicy: + if c.Spec.ParamKind != nil { + return fmt.Errorf("MutatingAdmissionPolicy %q: spec.paramKind is not supported for static manifests", name) + } + case *admissionregistrationv1.MutatingAdmissionPolicyBinding: + if err := validateBindingConstraints(name, c.Spec.PolicyName, c.Spec.ParamRef != nil); err != nil { + return err + } + default: + return fmt.Errorf("unsupported policy type %T", obj) + } + return nil +} + +// validateBindingConstraints checks common binding manifest constraints: +// policyName must be non-empty, have the static suffix, and paramRef must be nil. +func validateBindingConstraints(bindingName, policyName string, hasParamRef bool) error { + if len(policyName) == 0 { + return fmt.Errorf("binding %q must reference a policy (spec.policyName)", bindingName) + } + if !strings.HasSuffix(policyName, manifest.StaticConfigSuffix) { + return fmt.Errorf("binding %q: spec.policyName %q must end with %q", bindingName, policyName, manifest.StaticConfigSuffix) + } + if hasParamRef { + return fmt.Errorf("binding %q: spec.paramRef is not supported for static manifests", bindingName) + } + return nil +} + +// validateBindingReferences ensures all bindings reference policies that exist in the manifest set. +func validateBindingReferences[P, B metav1.Object](policies []P, bindings []B, getBindingPolicyName func(B) string) error { + policyNames := sets.New[string]() + for _, policy := range policies { + policyNames.Insert(policy.GetName()) + } + for _, binding := range bindings { + if !policyNames.Has(getBindingPolicyName(binding)) { + return fmt.Errorf("binding %q references policy %q which does not exist in the manifest directory", binding.GetName(), getBindingPolicyName(binding)) + } + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader_test.go new file mode 100644 index 000000000..b12decf5e --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/loader/loader_test.go @@ -0,0 +1,363 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "os" + "path/filepath" + "strings" + "testing" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var ( + testScheme = runtime.NewScheme() + testCodecs serializer.CodecFactory +) + +func init() { + utilruntime.Must(admissionregistrationv1.AddToScheme(testScheme)) + testScheme.AddUnversionedTypes(metav1.SchemeGroupVersion, &metav1.List{}, &metav1.Status{}) + testCodecs = serializer.NewCodecFactory(testScheme, serializer.EnableStrict) +} + +func acceptTestPolicy(obj runtime.Object) ([]*admissionregistrationv1.ValidatingAdmissionPolicy, error) { + switch p := obj.(type) { + case *admissionregistrationv1.ValidatingAdmissionPolicy: + return []*admissionregistrationv1.ValidatingAdmissionPolicy{p}, nil + case *admissionregistrationv1.ValidatingAdmissionPolicyList: + items := make([]*admissionregistrationv1.ValidatingAdmissionPolicy, len(p.Items)) + for i := range p.Items { + items[i] = &p.Items[i] + } + return items, nil + default: + return nil, ErrUnrecognizedType + } +} + +func acceptTestBinding(obj runtime.Object) ([]*admissionregistrationv1.ValidatingAdmissionPolicyBinding, error) { + switch b := obj.(type) { + case *admissionregistrationv1.ValidatingAdmissionPolicyBinding: + return []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{b}, nil + case *admissionregistrationv1.ValidatingAdmissionPolicyBindingList: + items := make([]*admissionregistrationv1.ValidatingAdmissionPolicyBinding, len(b.Items)) + for i := range b.Items { + items[i] = &b.Items[i] + } + return items, nil + default: + return nil, ErrUnrecognizedType + } +} + +func TestLoadPolicyManifests(t *testing.T) { + tests := []struct { + name string + files map[string]string + wantPolicies int + wantBindings int + wantErr bool + wantErrContain string + }{ + { + name: "load policy and binding", + files: map[string]string{ + "policy.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: test-policy.static.k8s.io +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +`, + "binding.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: test-binding.static.k8s.io +spec: + policyName: test-policy.static.k8s.io + validationActions: + - Deny +`, + }, + wantPolicies: 1, + wantBindings: 1, + }, + { + name: "empty directory", + files: map[string]string{}, + wantPolicies: 0, + wantBindings: 0, + }, + { + name: "binding references non-existent policy", + files: map[string]string{ + "binding.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: orphan.static.k8s.io +spec: + policyName: does-not-exist.static.k8s.io + validationActions: + - Deny +`, + }, + wantErr: true, + wantErrContain: "does not exist", + }, + { + name: "duplicate policy names", + files: map[string]string{ + "01-policy.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: dup.static.k8s.io +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +`, + "02-policy.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: dup.static.k8s.io +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +`, + }, + wantErr: true, + wantErrContain: "duplicate", + }, + { + name: "unsupported resource type", + files: map[string]string{ + "wrong.yaml": ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: not-a-policy +`, + }, + wantErr: true, + wantErrContain: "error loading", + }, + { + name: "v1.List with policy and binding", + files: map[string]string{ + "list.yaml": ` +apiVersion: v1 +kind: List +items: +- apiVersion: admissionregistration.k8s.io/v1 + kind: ValidatingAdmissionPolicy + metadata: + name: v1list-policy.static.k8s.io + spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +- apiVersion: admissionregistration.k8s.io/v1 + kind: ValidatingAdmissionPolicyBinding + metadata: + name: v1list-binding.static.k8s.io + spec: + policyName: v1list-policy.static.k8s.io + validationActions: + - Deny +`, + }, + wantPolicies: 1, + wantBindings: 1, + }, + { + name: "policy with paramKind rejected", + files: map[string]string{ + "policy.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: param-policy.static.k8s.io +spec: + paramKind: + apiVersion: v1 + kind: ConfigMap + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +`, + }, + wantErr: true, + wantErrContain: "paramKind is not supported", + }, + { + name: "binding with paramRef rejected", + files: map[string]string{ + "policy.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: ref-policy.static.k8s.io +spec: + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + validations: + - expression: "true" +`, + "binding.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: ref-binding.static.k8s.io +spec: + policyName: ref-policy.static.k8s.io + paramRef: + name: my-config + namespace: default + validationActions: + - Deny +`, + }, + wantErr: true, + wantErrContain: "paramRef", + }, + { + name: "binding policyName missing static suffix", + files: map[string]string{ + "binding.yaml": ` +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: bad-ref.static.k8s.io +spec: + policyName: some-policy-without-suffix + validationActions: + - Deny +`, + }, + wantErr: true, + wantErrContain: "must end with", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for name, content := range tt.files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatalf("failed to write file %s: %v", name, err) + } + } + + policies, bindings, _, err := LoadPolicyManifests( + dir, + testCodecs.UniversalDeserializer(), + acceptTestPolicy, + acceptTestBinding, + func(b *admissionregistrationv1.ValidatingAdmissionPolicyBinding) string { + return b.Spec.PolicyName + }, + ) + + if tt.wantErr { + if err == nil { + t.Error("expected error but got none") + } else if tt.wantErrContain != "" && !strings.Contains(err.Error(), tt.wantErrContain) { + t.Errorf("expected error containing %q, got %q", tt.wantErrContain, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(policies) != tt.wantPolicies { + t.Errorf("expected %d policies, got %d", tt.wantPolicies, len(policies)) + } + if len(bindings) != tt.wantBindings { + t.Errorf("expected %d bindings, got %d", tt.wantBindings, len(bindings)) + } + }) + } +} + +func TestValidateBindingReferences(t *testing.T) { + policies := []*admissionregistrationv1.ValidatingAdmissionPolicy{ + {ObjectMeta: metav1.ObjectMeta{Name: "policy-a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "policy-b"}}, + } + goodBindings := []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + {ObjectMeta: metav1.ObjectMeta{Name: "b1"}, Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{PolicyName: "policy-a"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "b2"}, Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{PolicyName: "policy-b"}}, + } + badBindings := []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + {ObjectMeta: metav1.ObjectMeta{Name: "b1"}, Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{PolicyName: "policy-missing"}}, + } + + getBindPolicy := func(b *admissionregistrationv1.ValidatingAdmissionPolicyBinding) string { + return b.Spec.PolicyName + } + + if err := validateBindingReferences(policies, goodBindings, getBindPolicy); err != nil { + t.Errorf("unexpected error for valid bindings: %v", err) + } + + if err := validateBindingReferences(policies, badBindings, getBindPolicy); err == nil { + t.Error("expected error for binding referencing non-existent policy") + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source.go new file mode 100644 index 000000000..0e37d7864 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source.go @@ -0,0 +1,192 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package source provides a Source implementation that loads policy configurations from manifest files. +package source + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission/plugin/manifest/metrics" + "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/util/filesystem" + "k8s.io/klog/v2" +) + +// defaultReloadInterval is the default interval at which the manifest directory is checked for changes. +var defaultReloadInterval = 1 * time.Minute + +// SetReloadIntervalForTests sets the reload interval for testing and returns a function to restore the original value. +func SetReloadIntervalForTests(interval time.Duration) func() { + original := defaultReloadInterval + defaultReloadInterval = interval + return func() { + defaultReloadInterval = original + } +} + +// PolicyLoadFunc loads policy and binding manifests from a directory. +type PolicyLoadFunc[P, B runtime.Object] func(dir string) ([]P, []B, string, error) + +// PolicyCompiler compiles a policy into an Evaluator. +type PolicyCompiler[P runtime.Object, E generic.Evaluator] func(P) (E, error) + +// BindingPolicyName extracts the policy name referenced by a binding. +type BindingPolicyName[B runtime.Object] func(B) string + +// StaticPolicySource provides policy configurations loaded from manifest files. +type StaticPolicySource[P, B runtime.Object, E generic.Evaluator] struct { + manifestsDir string + apiServerID string + reloadInterval time.Duration + compiler PolicyCompiler[P, E] + loadFunc PolicyLoadFunc[P, B] + getBindingPolicyName BindingPolicyName[B] + manifestType metrics.ManifestType + + current atomic.Pointer[[]generic.PolicyHook[P, B, E]] + lastReadHash atomic.Pointer[string] // hash of last file content read (for short-circuiting) + hasSynced atomic.Bool +} + +// NewStaticPolicySource creates a new static policy source that loads configurations from the specified directory. +func NewStaticPolicySource[P, B runtime.Object, E generic.Evaluator]( + manifestsDir, apiServerID string, + compiler PolicyCompiler[P, E], + loadFunc PolicyLoadFunc[P, B], + getBindingPolicyName BindingPolicyName[B], + manifestType metrics.ManifestType, +) *StaticPolicySource[P, B, E] { + metrics.RegisterMetrics() + return &StaticPolicySource[P, B, E]{ + manifestsDir: manifestsDir, + apiServerID: apiServerID, + reloadInterval: defaultReloadInterval, + compiler: compiler, + loadFunc: loadFunc, + getBindingPolicyName: getBindingPolicyName, + manifestType: manifestType, + } +} + +// LoadInitial performs the initial load of manifests. +// This should be called during API server startup and will fail if the manifests cannot be loaded. +func (s *StaticPolicySource[P, B, E]) LoadInitial() error { + policies, bindings, hash, err := s.loadFunc(s.manifestsDir) + if err != nil { + return err + } + + hooks, err := s.compile(policies, bindings) + if err != nil { + return err + } + s.current.Store(&hooks) + s.lastReadHash.Store(&hash) + s.hasSynced.Store(true) + + klog.InfoS("Loaded manifest-based admission policy configurations", "plugin", string(s.manifestType), "count", len(hooks)) + metrics.RecordAutomaticReloadSuccess(s.manifestType, s.apiServerID, hash) + return nil +} + +// RunReloadLoop watches for configuration changes and reloads when detected. +// It blocks until ctx is canceled. +func (s *StaticPolicySource[P, B, E]) RunReloadLoop(ctx context.Context) { + filesystem.WatchUntil( + ctx, + s.reloadInterval, + s.manifestsDir, + func() { + s.checkAndReload() + }, + func(err error) { + klog.ErrorS(err, "watching manifest directory", "plugin", string(s.manifestType), "dir", s.manifestsDir) + }, + ) +} + +func (s *StaticPolicySource[P, B, E]) compile(policies []P, bindings []B) ([]generic.PolicyHook[P, B, E], error) { + bindingsByPolicy := make(map[string][]B) + for _, binding := range bindings { + policyName := s.getBindingPolicyName(binding) + bindingsByPolicy[policyName] = append(bindingsByPolicy[policyName], binding) + } + + var hooks []generic.PolicyHook[P, B, E] + for _, policy := range policies { + nameGetter, ok := any(policy).(interface{ GetName() string }) + if !ok { + return nil, fmt.Errorf("policy type %T does not implement GetName()", policy) + } + evaluator, err := s.compiler(policy) + if err != nil { + return nil, fmt.Errorf("failed to compile policy %q: %w", nameGetter.GetName(), err) + } + hook := generic.PolicyHook[P, B, E]{ + Policy: policy, + Bindings: bindingsByPolicy[nameGetter.GetName()], + Evaluator: evaluator, + } + hooks = append(hooks, hook) + } + + return hooks, nil +} + +func (s *StaticPolicySource[P, B, E]) checkAndReload() { + policies, bindings, hash, err := s.loadFunc(s.manifestsDir) + if err != nil { + klog.ErrorS(err, "reloading manifest config", "plugin", string(s.manifestType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadFailure(s.manifestType, s.apiServerID) + return + } + + // Short-circuit if file content hasn't changed since last read. + if last := s.lastReadHash.Load(); last != nil && hash == *last { + return + } + s.lastReadHash.Store(&hash) + + hooks, err := s.compile(policies, bindings) + if err != nil { + klog.ErrorS(err, "compiling manifest config", "plugin", string(s.manifestType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadFailure(s.manifestType, s.apiServerID) + return + } + s.current.Store(&hooks) + + klog.InfoS("reloaded manifest config", "plugin", string(s.manifestType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadSuccess(s.manifestType, s.apiServerID, hash) +} + +// HasSynced returns true if the initial load has completed. +func (s *StaticPolicySource[P, B, E]) HasSynced() bool { + return s.hasSynced.Load() +} + +// Hooks returns the list of policy hooks. +func (s *StaticPolicySource[P, B, E]) Hooks() []generic.PolicyHook[P, B, E] { + current := s.current.Load() + if current == nil { + return nil + } + return *current +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source_test.go new file mode 100644 index 000000000..cc1bf1902 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/manifest/source/source_test.go @@ -0,0 +1,189 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "fmt" + "testing" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission/plugin/manifest/metrics" +) + +type mockEvaluator struct{} + +type mockPolicyLoadFunc struct { + policies []*admissionregistrationv1.ValidatingAdmissionPolicy + bindings []*admissionregistrationv1.ValidatingAdmissionPolicyBinding + hash string + err error + callCount int +} + +func (m *mockPolicyLoadFunc) load(dir string) ([]*admissionregistrationv1.ValidatingAdmissionPolicy, []*admissionregistrationv1.ValidatingAdmissionPolicyBinding, string, error) { + m.callCount++ + return m.policies, m.bindings, m.hash, m.err +} + +func validVAP(name string) *admissionregistrationv1.ValidatingAdmissionPolicy { + return &admissionregistrationv1.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } +} + +func validVAPB(name, policyName string) *admissionregistrationv1.ValidatingAdmissionPolicyBinding { + return &admissionregistrationv1.ValidatingAdmissionPolicyBinding{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: admissionregistrationv1.ValidatingAdmissionPolicyBindingSpec{PolicyName: policyName}, + } +} + +func mockCompiler(p *admissionregistrationv1.ValidatingAdmissionPolicy) (mockEvaluator, error) { + return mockEvaluator{}, nil +} + +func getBindingPolicyName(b *admissionregistrationv1.ValidatingAdmissionPolicyBinding) string { + return b.Spec.PolicyName +} + +func newTestSource(loadFunc PolicyLoadFunc[*admissionregistrationv1.ValidatingAdmissionPolicy, *admissionregistrationv1.ValidatingAdmissionPolicyBinding]) *StaticPolicySource[*admissionregistrationv1.ValidatingAdmissionPolicy, *admissionregistrationv1.ValidatingAdmissionPolicyBinding, mockEvaluator] { + return NewStaticPolicySource[*admissionregistrationv1.ValidatingAdmissionPolicy, *admissionregistrationv1.ValidatingAdmissionPolicyBinding, mockEvaluator]( + "/tmp/test-manifests", + "test-server", + mockCompiler, + loadFunc, + getBindingPolicyName, + metrics.VAPManifestType, + ) +} + +func TestStaticPolicySource_LoadInitial(t *testing.T) { + mock := &mockPolicyLoadFunc{ + policies: []*admissionregistrationv1.ValidatingAdmissionPolicy{validVAP("policy1")}, + bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{validVAPB("binding1", "policy1")}, + hash: "h1", + } + src := newTestSource(mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial() returned unexpected error: %v", err) + } + if !src.HasSynced() { + t.Error("HasSynced() = false, want true after LoadInitial") + } + + hooks := src.Hooks() + if len(hooks) != 1 { + t.Fatalf("Hooks() returned %d hooks, want 1", len(hooks)) + } + if hooks[0].Policy.Name != "policy1" { + t.Errorf("hook policy name = %q, want %q", hooks[0].Policy.Name, "policy1") + } + if len(hooks[0].Bindings) != 1 { + t.Fatalf("hook has %d bindings, want 1", len(hooks[0].Bindings)) + } + if hooks[0].Bindings[0].Name != "binding1" { + t.Errorf("hook binding name = %q, want %q", hooks[0].Bindings[0].Name, "binding1") + } + if mock.callCount != 1 { + t.Errorf("loadFunc called %d times, want 1", mock.callCount) + } +} + +func TestStaticPolicySource_HashSkipsReload(t *testing.T) { + mock := &mockPolicyLoadFunc{ + policies: []*admissionregistrationv1.ValidatingAdmissionPolicy{validVAP("policy1")}, + bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{validVAPB("binding1", "policy1")}, + hash: "h1", + } + src := newTestSource(mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial() returned unexpected error: %v", err) + } + hooksBefore := src.Hooks() + + // checkAndReload with the same hash should skip the update. + src.checkAndReload() + + if mock.callCount != 2 { + t.Errorf("loadFunc called %d times, want 2", mock.callCount) + } + hooksAfter := src.Hooks() + if len(hooksAfter) != len(hooksBefore) { + t.Errorf("Hooks() length changed from %d to %d after no-op reload", len(hooksBefore), len(hooksAfter)) + } + // Verify the pointer didn't change (same underlying slice). + if &hooksAfter[0] != &hooksBefore[0] { + t.Error("Hooks() returned a different slice after hash-matched reload; expected same slice") + } +} + +func TestStaticPolicySource_FilesDeletionClearsHooks(t *testing.T) { + mock := &mockPolicyLoadFunc{ + policies: []*admissionregistrationv1.ValidatingAdmissionPolicy{validVAP("policy1")}, + bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{validVAPB("binding1", "policy1")}, + hash: "h1", + } + src := newTestSource(mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial() returned unexpected error: %v", err) + } + if len(src.Hooks()) != 1 { + t.Fatalf("Hooks() returned %d hooks after initial load, want 1", len(src.Hooks())) + } + + // Simulate files being deleted: empty policies/bindings with a new hash. + mock.policies = nil + mock.bindings = nil + mock.hash = "h2" + + src.checkAndReload() + + hooks := src.Hooks() + if len(hooks) != 0 { + t.Errorf("Hooks() returned %d hooks after deletion reload, want 0", len(hooks)) + } +} + +func TestStaticPolicySource_ReloadKeepsPreviousOnError(t *testing.T) { + mock := &mockPolicyLoadFunc{ + policies: []*admissionregistrationv1.ValidatingAdmissionPolicy{validVAP("policy1")}, + bindings: []*admissionregistrationv1.ValidatingAdmissionPolicyBinding{validVAPB("binding1", "policy1")}, + hash: "h1", + } + src := newTestSource(mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial() returned unexpected error: %v", err) + } + + // Make loadFunc return an error on subsequent calls. + mock.err = fmt.Errorf("simulated load error") + + src.checkAndReload() + + hooks := src.Hooks() + if len(hooks) != 1 { + t.Fatalf("Hooks() returned %d hooks after error reload, want 1", len(hooks)) + } + if hooks[0].Policy.Name != "policy1" { + t.Errorf("hook policy name = %q, want %q", hooks[0].Policy.Name, "policy1") + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/matching/matching.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/matching/matching.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/matching/matching.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/matching/matching.go index eebe76943..30a6cbebe 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/matching/matching.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/matching/matching.go @@ -17,6 +17,7 @@ limitations under the License. package matching import ( + "context" "fmt" v1 "k8s.io/api/admissionregistration/v1" @@ -44,8 +45,8 @@ type Matcher struct { objectMatcher *object.Matcher } -func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) { - return m.namespaceMatcher.GetNamespace(name) +func (m *Matcher) GetNamespace(ctx context.Context, name string) (*corev1.Namespace, error) { + return m.namespaceMatcher.GetNamespace(ctx, name) } // NewMatcher initialize the matcher with dependencies requires diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/matching/matching_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/matching/matching_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/matching/matching_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/matching/matching_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/accessor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/accessor.go similarity index 69% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/accessor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/accessor.go index ff342623a..b20103c57 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/accessor.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/accessor.go @@ -18,7 +18,6 @@ package mutating import ( v1 "k8s.io/api/admissionregistration/v1" - "k8s.io/api/admissionregistration/v1beta1" "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/admission/plugin/policy/generic" ) @@ -59,19 +58,11 @@ func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind { } func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources { - return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints) + return v.Spec.MatchConstraints } func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType { - return toV1FailurePolicy(v.Spec.FailurePolicy) -} - -func toV1FailurePolicy(failurePolicy *v1beta1.FailurePolicyType) *v1.FailurePolicyType { - if failurePolicy == nil { - return nil - } - fp := v1.FailurePolicyType(*failurePolicy) - return &fp + return v.Spec.FailurePolicy } type mutatingAdmissionPolicyBindingAccessor struct { @@ -94,7 +85,7 @@ func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.Namespace } func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources { - return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources) + return v.Spec.MatchResources } func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef { @@ -115,30 +106,3 @@ func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef { ParameterNotFoundAction: nfa, } } - -func convertV1alpha1ResourceRulesToV1(mc *v1beta1.MatchResources) *v1.MatchResources { - if mc == nil { - return nil - } - - var res v1.MatchResources - res.NamespaceSelector = mc.NamespaceSelector - res.ObjectSelector = mc.ObjectSelector - for _, ex := range mc.ExcludeResourceRules { - res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{ - ResourceNames: ex.ResourceNames, - RuleWithOperations: ex.RuleWithOperations, - }) - } - for _, ex := range mc.ResourceRules { - res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{ - ResourceNames: ex.ResourceNames, - RuleWithOperations: ex.RuleWithOperations, - }) - } - if mc.MatchPolicy != nil { - mp := v1.MatchPolicyType(*mc.MatchPolicy) - res.MatchPolicy = &mp - } - return &res -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation.go similarity index 76% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation.go index d8b5c11a5..a0772aab4 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation.go @@ -19,9 +19,10 @@ package mutating import ( "fmt" - "k8s.io/api/admissionregistration/v1beta1" + "k8s.io/api/admissionregistration/v1" plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" + "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/environment" @@ -32,9 +33,9 @@ import ( // // Each individual mutation is compiled into MutationEvaluationFunc and // returned is a PolicyEvaluator in the same order as the mutations appeared in the policy. -func compilePolicy(policy *Policy) PolicyEvaluator { - opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true} - compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) +func compilePolicy(policy *v1.MutatingAdmissionPolicy) PolicyEvaluator { + opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, HasAuthorizer: true} + compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) if err != nil { return PolicyEvaluator{Error: &apiservercel.Error{ Type: apiservercel.ErrorTypeInternal, @@ -43,7 +44,7 @@ func compilePolicy(policy *Policy) PolicyEvaluator { } // Compile and store variables - compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions) + compiler.CompileAndStoreVariables(convertV1Variables(policy.Spec.Variables), opts, environment.StoredExpressions) // Compile matchers var matcher matchconditions.Matcher = nil @@ -53,7 +54,7 @@ func compilePolicy(policy *Policy) PolicyEvaluator { for i := range matchConditions { matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i]) } - matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name) + matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), policy.Spec.FailurePolicy, "policy", "validate", policy.Name) } // Compiler patchers @@ -62,13 +63,13 @@ func compilePolicy(policy *Policy) PolicyEvaluator { patchOptions.HasPatchTypes = true for _, m := range policy.Spec.Mutations { switch m.PatchType { - case v1beta1.PatchTypeJSONPatch: + case v1.PatchTypeJSONPatch: if m.JSONPatch != nil { accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression} compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) patchers = append(patchers, patch.NewJSONPatcher(compileResult)) } - case v1beta1.PatchTypeApplyConfiguration: + case v1.PatchTypeApplyConfiguration: if m.ApplyConfiguration != nil { accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression} compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions) @@ -77,5 +78,19 @@ func compilePolicy(policy *Policy) PolicyEvaluator { } } - return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv} + return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositedCompiler: compiler} +} + +func convertV1Variables(variables []v1.Variable) []plugincel.NamedExpressionAccessor { + if variables == nil { + return nil + } + res := make([]plugincel.NamedExpressionAccessor, len(variables)) + for i, v := range variables { + res[i] = &validating.Variable{ + Name: v.Name, + Expression: v.Expression, + } + } + return res } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation_test.go similarity index 82% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation_test.go index c5be5befc..296584b25 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/compilation_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/compilation_test.go @@ -18,12 +18,13 @@ package mutating import ( "context" - "github.com/google/go-cmp/cmp" "strings" "testing" "time" - "k8s.io/api/admissionregistration/v1beta1" + "github.com/google/go-cmp/cmp" + + v1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -49,7 +50,7 @@ func TestCompilation(t *testing.T) { deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"} testCases := []struct { name string - policy *Policy + policy *v1.MutatingAdmissionPolicy gvr schema.GroupVersionResource object runtime.Object oldObject runtime.Object @@ -60,9 +61,9 @@ func TestCompilation(t *testing.T) { }{ { name: "applyConfiguration then jsonPatch", - policy: mutations(policy("d1"), v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + policy: mutations(policy("d1"), v1.Mutation{ + PatchType: v1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1.ApplyConfiguration{ Expression: `Object{ spec: Object.spec{ replicas: object.spec.replicas + 100 @@ -70,9 +71,9 @@ func TestCompilation(t *testing.T) { }`, }, }, - v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeJSONPatch, - JSONPatch: &v1beta1.JSONPatch{ + v1.Mutation{ + PatchType: v1.PatchTypeJSONPatch, + JSONPatch: &v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10} ]`, @@ -85,17 +86,17 @@ func TestCompilation(t *testing.T) { { name: "jsonPatch then applyConfiguration", policy: mutations(policy("d1"), - v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeJSONPatch, - JSONPatch: &v1beta1.JSONPatch{ + v1.Mutation{ + PatchType: v1.PatchTypeJSONPatch, + JSONPatch: &v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10} ]`, }, }, - v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + v1.Mutation{ + PatchType: v1.PatchTypeApplyConfiguration, + ApplyConfiguration: &v1.ApplyConfiguration{ Expression: `Object{ spec: Object.spec{ replicas: object.spec.replicas + 100 @@ -109,7 +110,7 @@ func TestCompilation(t *testing.T) { }, { name: "jsonPatch with variable", - policy: jsonPatches(variables(policy("d1"), v1beta1.Variable{Name: "desired", Expression: "10"}), v1beta1.JSONPatch{ + policy: jsonPatches(variables(policy("d1"), v1.Variable{Name: "desired", Expression: "10"}), v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: variables.desired + 1}, ]`, @@ -120,7 +121,7 @@ func TestCompilation(t *testing.T) { }, { name: "apply configuration with variable", - policy: applyConfigurations(variables(policy("d1"), v1beta1.Variable{Name: "desired", Expression: "10"}), + policy: applyConfigurations(variables(policy("d1"), v1.Variable{Name: "desired", Expression: "10"}), `Object{ spec: Object.spec{ replicas: variables.desired + 1 @@ -137,7 +138,7 @@ func TestCompilation(t *testing.T) { spec: Object.spec{ replicas: int(params.data['k1']) } - }`), &v1beta1.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}), + }`), &v1.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}), params: &corev1.ConfigMap{Data: map[string]string{"k1": "100"}}, gvr: deploymentGVR, object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, @@ -145,7 +146,7 @@ func TestCompilation(t *testing.T) { }, { name: "jsonPatch with excessive cost", - policy: jsonPatches(variables(policy("d1"), v1beta1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1beta1.JSONPatch{ + policy: jsonPatches(variables(policy("d1"), v1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0 @@ -163,14 +164,14 @@ func TestCompilation(t *testing.T) { spec: Object.spec{ replicas: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0 } - }`), v1beta1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), + }`), v1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), gvr: deploymentGVR, object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, expectedErr: "operation cancelled: actual cost limit exceeded", }, { name: "request variable", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: request.kind.group == 'apps' && request.kind.version == 'v1' && request.kind.kind == 'Deployment' ? 10 : 0 @@ -182,7 +183,7 @@ func TestCompilation(t *testing.T) { }, { name: "namespace request variable", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: namespaceObject.metadata.name == 'ns1' ? 10 : 0 @@ -195,7 +196,7 @@ func TestCompilation(t *testing.T) { }, { name: "authorizer check", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{op: "replace", path: "/spec/replicas", value: authorizer.group('').resource('endpoints').check('create').allowed() ? 10 : 0 @@ -208,7 +209,7 @@ func TestCompilation(t *testing.T) { }, { name: "object type has field access", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{ op: "add", path: "/metadata/labels", @@ -226,7 +227,7 @@ func TestCompilation(t *testing.T) { }, { name: "object type has field testing", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{ op: "add", path: "/metadata/labels", @@ -246,7 +247,7 @@ func TestCompilation(t *testing.T) { }, { name: "object type equality", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{ op: "add", path: "/metadata/labels", @@ -274,7 +275,7 @@ func TestCompilation(t *testing.T) { // recompile all expressions with fully bound types before evaluation and report // errors if invalid Object types like this are initialized. name: "object types are not fully type checked", - policy: jsonPatches(policy("d1"), v1beta1.JSONPatch{ + policy: jsonPatches(policy("d1"), v1.JSONPatch{ Expression: `[ JSONPatch{ op: "add", path: "/spec", @@ -326,8 +327,8 @@ func TestCompilation(t *testing.T) { } policyEvaluator := compilePolicy(tc.policy) - if policyEvaluator.CompositionEnv != nil { - ctx = policyEvaluator.CompositionEnv.CreateContext(ctx) + if policyEvaluator.CompositedCompiler != nil { + ctx = policyEvaluator.CompositedCompiler.CreateContext(ctx) } obj := tc.object @@ -400,52 +401,52 @@ func TestCompilation(t *testing.T) { } } -func policy(name string) *v1beta1.MutatingAdmissionPolicy { - return &v1beta1.MutatingAdmissionPolicy{ +func policy(name string) *v1.MutatingAdmissionPolicy { + return &v1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, - Spec: v1beta1.MutatingAdmissionPolicySpec{}, + Spec: v1.MutatingAdmissionPolicySpec{}, } } -func variables(policy *v1beta1.MutatingAdmissionPolicy, variables ...v1beta1.Variable) *v1beta1.MutatingAdmissionPolicy { +func variables(policy *v1.MutatingAdmissionPolicy, variables ...v1.Variable) *v1.MutatingAdmissionPolicy { policy.Spec.Variables = append(policy.Spec.Variables, variables...) return policy } -func jsonPatches(policy *v1beta1.MutatingAdmissionPolicy, jsonPatches ...v1beta1.JSONPatch) *v1beta1.MutatingAdmissionPolicy { +func jsonPatches(policy *v1.MutatingAdmissionPolicy, jsonPatches ...v1.JSONPatch) *v1.MutatingAdmissionPolicy { for _, jsonPatch := range jsonPatches { - policy.Spec.Mutations = append(policy.Spec.Mutations, v1beta1.Mutation{ + policy.Spec.Mutations = append(policy.Spec.Mutations, v1.Mutation{ JSONPatch: &jsonPatch, - PatchType: v1beta1.PatchTypeJSONPatch, + PatchType: v1.PatchTypeJSONPatch, }) } return policy } -func applyConfigurations(policy *v1beta1.MutatingAdmissionPolicy, expressions ...string) *v1beta1.MutatingAdmissionPolicy { +func applyConfigurations(policy *v1.MutatingAdmissionPolicy, expressions ...string) *v1.MutatingAdmissionPolicy { for _, expression := range expressions { - policy.Spec.Mutations = append(policy.Spec.Mutations, v1beta1.Mutation{ - ApplyConfiguration: &v1beta1.ApplyConfiguration{Expression: expression}, - PatchType: v1beta1.PatchTypeApplyConfiguration, + policy.Spec.Mutations = append(policy.Spec.Mutations, v1.Mutation{ + ApplyConfiguration: &v1.ApplyConfiguration{Expression: expression}, + PatchType: v1.PatchTypeApplyConfiguration, }) } return policy } -func paramKind(policy *v1beta1.MutatingAdmissionPolicy, paramKind *v1beta1.ParamKind) *v1beta1.MutatingAdmissionPolicy { +func paramKind(policy *v1.MutatingAdmissionPolicy, paramKind *v1.ParamKind) *v1.MutatingAdmissionPolicy { policy.Spec.ParamKind = paramKind return policy } -func mutations(policy *v1beta1.MutatingAdmissionPolicy, mutations ...v1beta1.Mutation) *v1beta1.MutatingAdmissionPolicy { +func mutations(policy *v1.MutatingAdmissionPolicy, mutations ...v1.Mutation) *v1.MutatingAdmissionPolicy { policy.Spec.Mutations = append(policy.Spec.Mutations, mutations...) return policy } -func matchConstraints(policy *v1beta1.MutatingAdmissionPolicy, matchConstraints *v1beta1.MatchResources) *v1beta1.MutatingAdmissionPolicy { +func matchConstraints(policy *v1.MutatingAdmissionPolicy, matchConstraints *v1.MatchResources) *v1.MutatingAdmissionPolicy { policy.Spec.MatchConstraints = matchConstraints return policy } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher.go index 153b268d6..e5b10c7f6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher.go @@ -22,7 +22,7 @@ import ( "fmt" "time" - "k8s.io/api/admissionregistration/v1beta1" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" v1 "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -122,8 +122,12 @@ func (d *dispatcher) dispatchInvocations( // if it is cluster scoped, namespaceName will be empty // Otherwise, get the Namespace resource. if namespaceName != "" { - namespace, err = d.matcher.GetNamespace(namespaceName) + namespace, err = d.matcher.GetNamespace(ctx, namespaceName) if err != nil { + var statusError *k8serrors.StatusError + if errors.As(err, &statusError) { + return nil, statusError + } return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName) } } @@ -133,8 +137,8 @@ func (d *dispatcher) dispatchInvocations( // Should loop through invocations, handling possible error and invoking // evaluator to apply patch, also should handle re-invocations for _, invocation := range invocations { - if invocation.Evaluator.CompositionEnv != nil { - ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx) + if invocation.Evaluator.CompositedCompiler != nil { + ctx = invocation.Evaluator.CompositedCompiler.CreateContext(ctx) } if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) { // This would be a bug. The compiler should always return exactly as @@ -206,7 +210,7 @@ func (d *dispatcher) dispatchInvocations( policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins() reinvokeCtx.SetShouldReinvoke() } - if invocation.Policy.Spec.ReinvocationPolicy == v1beta1.IfNeededReinvocationPolicy { + if invocation.Policy.Spec.ReinvocationPolicy == admissionregistrationv1.IfNeededReinvocationPolicy { policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey) } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go similarity index 72% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go index d4f01266c..7885d3144 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/dispatcher_test.go @@ -23,7 +23,6 @@ import ( "time" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" - "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -33,7 +32,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/policy/generic" "k8s.io/apiserver/pkg/admission/plugin/policy/matching" "k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" "k8s.io/client-go/informers" @@ -75,16 +73,16 @@ func TestDispatcher(t *testing.T) { }, }, }}, - policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ + policyHooks: []PolicyHook{ { - Policy: mutations(matchConstraints(policy("policy1"), &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Policy: mutations(matchConstraints(policy("policy1"), &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -93,18 +91,18 @@ func TestDispatcher(t *testing.T) { }, }, }, - }), v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + }), admissionregistrationv1.Mutation{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ spec: Object.spec{ replicas: object.spec.replicas + 100 } }`, }}), - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy1", }, }}, @@ -166,16 +164,16 @@ func TestDispatcher(t *testing.T) { }, }, }, - policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ + policyHooks: []PolicyHook{ { - Policy: paramKind(mutations(matchConstraints(policy("policy1"), &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Policy: paramKind(mutations(matchConstraints(policy("policy1"), &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -184,24 +182,24 @@ func TestDispatcher(t *testing.T) { }, }, }}), - v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + admissionregistrationv1.Mutation{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ spec: Object.spec{ replicas: object.spec.replicas + int(params.data['key']) } }`, }}), - &v1beta1.ParamKind{ + &admissionregistrationv1.ParamKind{ APIVersion: "v1", Kind: "ConfigMap", }), - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy1", - ParamRef: &v1beta1.ParamRef{Name: "cm1", Namespace: "default"}, + ParamRef: &admissionregistrationv1.ParamRef{Name: "cm1", Namespace: "default"}, }, }}, }, @@ -246,16 +244,16 @@ func TestDispatcher(t *testing.T) { Type: appsv1.RollingUpdateDeploymentStrategyType, }, }}, - policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ + policyHooks: []PolicyHook{ { - Policy: mutations(matchConstraints(policy("policy1"), &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Policy: mutations(matchConstraints(policy("policy1"), &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -264,31 +262,31 @@ func TestDispatcher(t *testing.T) { }, }, }, - }), v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + }), admissionregistrationv1.Mutation{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"policy1": string(int(object.?metadata.labels["count"].orValue("1")) + 1)} } }`, }}), - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy1", }, }}, }, { - Policy: mutations(matchConstraints(policy("policy2"), &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Policy: mutations(matchConstraints(policy("policy2"), &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -297,18 +295,18 @@ func TestDispatcher(t *testing.T) { }, }, }, - }), v1beta1.Mutation{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + }), admissionregistrationv1.Mutation{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"policy2": string(int(object.?metadata.labels["count"].orValue("1")) + 1)} } }`, }}), - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy2", }, }}, @@ -357,21 +355,21 @@ func TestDispatcher(t *testing.T) { Type: appsv1.RollingUpdateDeploymentStrategyType, }, }}, - policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ + policyHooks: []PolicyHook{ { - Policy: &v1beta1.MutatingAdmissionPolicy{ + Policy: &admissionregistrationv1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "policy1", }, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: admissionregistrationv1.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -381,9 +379,9 @@ func TestDispatcher(t *testing.T) { }, }, }, - Mutations: []v1beta1.Mutation{{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + Mutations: []admissionregistrationv1.Mutation{{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"environment": "production"} @@ -392,27 +390,27 @@ func TestDispatcher(t *testing.T) { }, }, }, - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy1", }, }}, }, { - Policy: &v1beta1.MutatingAdmissionPolicy{ + Policy: &admissionregistrationv1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "policy2", }, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: admissionregistrationv1.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -422,15 +420,15 @@ func TestDispatcher(t *testing.T) { }, }, }, - MatchConditions: []v1beta1.MatchCondition{ + MatchConditions: []admissionregistrationv1.MatchCondition{ { Name: "prodonly", Expression: `object.?metadata.labels["environment"].orValue("") == "production"`, }, }, - Mutations: []v1beta1.Mutation{{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + Mutations: []admissionregistrationv1.Mutation{{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"policy1invoked": "true"} @@ -439,9 +437,9 @@ func TestDispatcher(t *testing.T) { }, }, }, - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy2", }, }}, @@ -491,21 +489,21 @@ func TestDispatcher(t *testing.T) { Type: appsv1.RollingUpdateDeploymentStrategyType, }, }}, - policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{ + policyHooks: []PolicyHook{ { - Policy: &v1beta1.MutatingAdmissionPolicy{ + Policy: &admissionregistrationv1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "policy1", }, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: admissionregistrationv1.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -515,15 +513,15 @@ func TestDispatcher(t *testing.T) { }, }, }, - MatchConditions: []v1beta1.MatchCondition{ + MatchConditions: []admissionregistrationv1.MatchCondition{ { Name: "prodonly", Expression: `object.?metadata.labels["environment"].orValue("") == "production"`, }, }, - Mutations: []v1beta1.Mutation{{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + Mutations: []admissionregistrationv1.Mutation{{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"policy1invoked": "true"} @@ -532,27 +530,27 @@ func TestDispatcher(t *testing.T) { }, }, }, - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy1", }, }}, }, { - Policy: &v1beta1.MutatingAdmissionPolicy{ + Policy: &admissionregistrationv1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "policy2", }, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: admissionregistrationv1.MutatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1.MatchResources{ + MatchPolicy: ptr.To(admissionregistrationv1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, - ResourceRules: []v1beta1.NamedRuleWithOperations{ + ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { - RuleWithOperations: v1beta1.RuleWithOperations{ - Rule: v1beta1.Rule{ + RuleWithOperations: admissionregistrationv1.RuleWithOperations{ + Rule: admissionregistrationv1.Rule{ APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, @@ -562,9 +560,9 @@ func TestDispatcher(t *testing.T) { }, }, }, - Mutations: []v1beta1.Mutation{{ - PatchType: v1beta1.PatchTypeApplyConfiguration, - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + Mutations: []admissionregistrationv1.Mutation{{ + PatchType: admissionregistrationv1.PatchTypeApplyConfiguration, + ApplyConfiguration: &admissionregistrationv1.ApplyConfiguration{ Expression: `Object{ metadata: Object.metadata{ labels: {"environment": "production"} @@ -573,9 +571,9 @@ func TestDispatcher(t *testing.T) { }, }, }, - Bindings: []*PolicyBinding{{ + Bindings: []*admissionregistrationv1.MutatingAdmissionPolicyBinding{{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: admissionregistrationv1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy2", }, }}, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/json_patch.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/json_patch.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/json_patch.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/json_patch.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go index 121ff4bba..151719980 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/json_patch_test.go @@ -18,10 +18,11 @@ package patch import ( "context" - "github.com/google/go-cmp/cmp" "strings" "testing" + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -403,7 +404,7 @@ func TestJSONPatch(t *testing.T) { }, } - compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) if err != nil { t.Fatal(err) @@ -411,7 +412,7 @@ func TestJSONPatch(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { accessor := &JSONPatchCondition{Expression: tc.expression} - compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions) + compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{HasPatchTypes: true}, environment.StoredExpressions) patcher := jsonPatcher{PatchEvaluator: compileResult} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd.go index 3be79df54..7135aca3a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd.go @@ -138,7 +138,7 @@ func ApplyStructuredMergeDiff( return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err) } - liveObjTyped, err := typeConverter.ObjectToTyped(originalObject) + liveObjTyped, err := typeConverter.ObjectToTyped(originalObject, typed.AllowDuplicates) if err != nil { return nil, fmt.Errorf("failed to convert original object to typed object: %w", err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go similarity index 78% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go index 9ad483b3c..3f0b87233 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/smd_test.go @@ -18,11 +18,12 @@ package patch import ( "context" - "github.com/google/go-cmp/cmp" "strings" "testing" "time" + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -232,9 +233,110 @@ func TestApplyConfiguration(t *testing.T) { object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}}, expectedErr: "must evaluate to Object but got Object.spec.metadata", }, + { + name: "apply configuration with duplicate env vars in original object", + expression: `Object{ + spec: Object.spec{ + replicas: 3 + } + }`, + gvr: deploymentGVR, + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To[int32](1), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", + Env: []corev1.EnvVar{ + {Name: "test", Value: "a"}, + {Name: "test", Value: "b"}, + }, + }}, + }, + }, + }, + }, + expectedResult: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To[int32](3), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", + Env: []corev1.EnvVar{ + {Name: "test", Value: "a"}, + {Name: "test", Value: "b"}, + }, + }}, + }, + }, + }, + }, + }, + // This test verifies that modifying an existing environment variable works correctly, + // even if the original object contains duplicates. + // Because 'env' is defined with `listType=map` and `listMapKey=name` in the schema, + // Structured Merge Diff (SMD) treats 'name' as the unique key. + // When the patch updates the entry with name="test", SMD merges the changes. + // As a side effect of the merge process on a map-type list, duplicate entries for the + // same key in the original object are consolidated into a single entry in the result. + // This matches the behavior of Server-Side Apply (SSA) and kubectl apply. + { + name: "apply configuration modify existing env variable", + expression: `Object{ + spec: Object.spec{ + template: Object.spec.template{ + spec: Object.spec.template.spec{ + containers: [Object.spec.template.spec.containers{ + name: "nginx", + env: [Object.spec.template.spec.containers.env{ + name: "test", + value: "c" + }] + }] + } + } + } + }`, + gvr: deploymentGVR, + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To[int32](1), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", + Env: []corev1.EnvVar{ + {Name: "test", Value: "a"}, + {Name: "test", Value: "b"}, + {Name: "foo", Value: "bar"}, + }, + }}, + }, + }, + }, + }, + expectedResult: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To[int32](1), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "nginx", + Env: []corev1.EnvVar{ + {Name: "test", Value: "c"}, + {Name: "foo", Value: "bar"}, + }, + }}, + }, + }, + }, + }, + }, } - compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) if err != nil { t.Fatal(err) } @@ -255,7 +357,7 @@ func TestApplyConfiguration(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { accessor := &ApplyConfigurationCondition{Expression: tc.expression} - compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions) + compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{HasPatchTypes: true}, environment.StoredExpressions) patcher := applyConfigPatcher{expressionEvaluator: compileResult} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/typeconverter.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/typeconverter.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/typeconverter.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/typeconverter.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/patch/typeconverter_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin.go similarity index 67% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin.go index 20e4d2d0c..01dd230de 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin.go @@ -18,18 +18,23 @@ package mutating import ( "context" - celgo "github.com/google/cel-go/cel" "io" - "k8s.io/api/admissionregistration/v1beta1" + celgo "github.com/google/cel-go/cel" + + v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/managedfields" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/manifest/metrics" + "k8s.io/apiserver/pkg/admission/plugin/policy/config" "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/manifest/source" "k8s.io/apiserver/pkg/admission/plugin/policy/matching" "k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" @@ -49,13 +54,13 @@ const ( // Register registers a plugin func Register(plugins *admission.Plugins) { plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) { - return NewPlugin(configFile), nil + return NewPlugin(configFile) }) } -type Policy = v1beta1.MutatingAdmissionPolicy -type PolicyBinding = v1beta1.MutatingAdmissionPolicyBinding -type PolicyMutation = v1beta1.Mutation +type Policy = v1.MutatingAdmissionPolicy +type PolicyBinding = v1.MutatingAdmissionPolicyBinding + type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator] type Mutator struct { @@ -73,10 +78,10 @@ type MutationEvaluationFunc func( ) (runtime.Object, error) type PolicyEvaluator struct { - Matcher matchconditions.Matcher - Mutators []patch.Patcher - CompositionEnv *cel.CompositionEnv - Error error + Matcher matchconditions.Matcher + Mutators []patch.Patcher + CompositedCompiler *cel.CompositedCompiler + Error error } // Plugin is an implementation of admission.Interface. @@ -86,9 +91,43 @@ type Plugin struct { var _ admission.Interface = &Plugin{} var _ admission.MutationInterface = &Plugin{} +var _ initializer.WantsManifestLoaders = &Plugin{} + +// SetManifestLoaders provides the manifest load functions for scheme-based defaulting and validation. +func (a *Plugin) SetManifestLoaders(loaders *initializer.ManifestLoaders) { + if loaders == nil || loaders.LoadMutatingPolicyManifests == nil { + return + } + loadFunc := loaders.LoadMutatingPolicyManifests + a.SetStaticSourceFactory(func(manifestsDir string) (generic.ReloadableSource[PolicyHook], error) { + staticSource := source.NewStaticPolicySource(manifestsDir, a.GetAPIServerID(), + func(p *v1.MutatingAdmissionPolicy) (PolicyEvaluator, error) { + e := compilePolicy(p) + if e.Error != nil { + return PolicyEvaluator{}, e.Error + } + return e, nil + }, + func(dir string) ([]*v1.MutatingAdmissionPolicy, []*v1.MutatingAdmissionPolicyBinding, string, error) { + return loadFunc(dir) + }, + func(b *v1.MutatingAdmissionPolicyBinding) string { return b.Spec.PolicyName }, + metrics.MAPManifestType, + ) + if err := staticSource.LoadInitial(); err != nil { + return nil, err + } + return staticSource, nil + }) +} // NewPlugin returns a generic admission webhook plugin. -func NewPlugin(_ io.Reader) *Plugin { +func NewPlugin(configFile io.Reader) (*Plugin, error) { + cfg, err := config.LoadMutatingConfig(configFile) + if err != nil { + return nil, err + } + // There is no request body to mutate for DELETE, so this plugin never handles that operation. handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect) res := &Plugin{} @@ -96,8 +135,8 @@ func NewPlugin(_ io.Reader) *Plugin { handler, func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] { return generic.NewPolicySource( - f.Admissionregistration().V1beta1().MutatingAdmissionPolicies().Informer(), - f.Admissionregistration().V1beta1().MutatingAdmissionPolicyBindings().Informer(), + f.Admissionregistration().V1().MutatingAdmissionPolicies().Informer(), + f.Admissionregistration().V1().MutatingAdmissionPolicyBindings().Informer(), NewMutatingAdmissionPolicyAccessor, NewMutatingAdmissionPolicyBindingAccessor, compilePolicy, @@ -112,7 +151,8 @@ func NewPlugin(_ io.Reader) *Plugin { return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3())) }, ) - return res + res.SetStaticManifestsDir(cfg.StaticManifestsDir) + return res, nil } // Admit makes an admission decision based on the request attributes. @@ -141,11 +181,3 @@ func (v *Variable) ReturnTypes() []*celgo.Type { func (v *Variable) GetName() string { return v.Name } - -func convertv1alpha1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor { - namedExpressions := make([]cel.NamedExpressionAccessor, len(variables)) - for i, variable := range variables { - namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression} - } - return namedExpressions -} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin_test.go similarity index 78% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin_test.go index e57f17ce8..2ab6cfd8f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/plugin_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/plugin_test.go @@ -22,7 +22,7 @@ import ( "github.com/stretchr/testify/require" - "k8s.io/api/admissionregistration/v1beta1" + v1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,10 +42,10 @@ import ( func setupTest( t *testing.T, - compiler func(*mutating.Policy) mutating.PolicyEvaluator, -) *generic.PolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator] { + compiler func(*v1.MutatingAdmissionPolicy) mutating.PolicyEvaluator, +) *generic.PolicyTestContext[*v1.MutatingAdmissionPolicy, *v1.MutatingAdmissionPolicyBinding, mutating.PolicyEvaluator] { - testContext, testCancel, err := generic.NewPolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator]( + testContext, testCancel, err := generic.NewPolicyTestContext[*v1.MutatingAdmissionPolicy, *v1.MutatingAdmissionPolicyBinding, mutating.PolicyEvaluator]( t, mutating.NewMutatingAdmissionPolicyAccessor, mutating.NewMutatingAdmissionPolicyBindingAccessor, @@ -81,33 +81,33 @@ func TestBasicPatch(t *testing.T) { expectedAnnotations := map[string]string{"foo": "bar"} // Treat all policies as setting foo annotation to bar - testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { + testContext := setupTest(t, func(p *v1.MutatingAdmissionPolicy) mutating.PolicyEvaluator { return mutating.PolicyEvaluator{Mutators: []patch.Patcher{annotationPatcher{expectedAnnotations}}} }) // Set up a policy and binding that match, no params require.NoError(t, testContext.UpdateAndWait( - &mutating.Policy{ + &v1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "policy"}, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: v1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1.MatchResources{ + MatchPolicy: ptr.To(v1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, }, - Mutations: []v1beta1.Mutation{ + Mutations: []v1.Mutation{ { - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + ApplyConfiguration: &v1.ApplyConfiguration{ Expression: "ignored, but required", }, - PatchType: v1beta1.PatchTypeApplyConfiguration, + PatchType: v1.PatchTypeApplyConfiguration, }, }, }, }, - &mutating.PolicyBinding{ + &v1.MutatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: v1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy", }, }, @@ -136,7 +136,7 @@ func TestJSONPatch(t *testing.T) { }, } - testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { + testContext := setupTest(t, func(p *v1.MutatingAdmissionPolicy) mutating.PolicyEvaluator { return mutating.PolicyEvaluator{ Mutators: []patch.Patcher{smdPatcher{patch: patchObj}}, } @@ -144,27 +144,27 @@ func TestJSONPatch(t *testing.T) { // Set up a policy and binding that match, no params require.NoError(t, testContext.UpdateAndWait( - &mutating.Policy{ + &v1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "policy"}, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: v1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1.MatchResources{ + MatchPolicy: ptr.To(v1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, }, - Mutations: []v1beta1.Mutation{ + Mutations: []v1.Mutation{ { - JSONPatch: &v1beta1.JSONPatch{ + JSONPatch: &v1.JSONPatch{ Expression: "ignored, but required", }, - PatchType: v1beta1.PatchTypeApplyConfiguration, + PatchType: v1.PatchTypeApplyConfiguration, }, }, }, }, - &mutating.PolicyBinding{ + &v1.MutatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: v1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy", }, }, @@ -198,7 +198,7 @@ func TestSSAPatch(t *testing.T) { }, } - testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { + testContext := setupTest(t, func(p *v1.MutatingAdmissionPolicy) mutating.PolicyEvaluator { return mutating.PolicyEvaluator{ Mutators: []patch.Patcher{smdPatcher{patch: patchObj}}, } @@ -206,27 +206,27 @@ func TestSSAPatch(t *testing.T) { // Set up a policy and binding that match, no params require.NoError(t, testContext.UpdateAndWait( - &mutating.Policy{ + &v1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "policy"}, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: v1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1.MatchResources{ + MatchPolicy: ptr.To(v1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, }, - Mutations: []v1beta1.Mutation{ + Mutations: []v1.Mutation{ { - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + ApplyConfiguration: &v1.ApplyConfiguration{ Expression: "ignored, but required", }, - PatchType: v1beta1.PatchTypeApplyConfiguration, + PatchType: v1.PatchTypeApplyConfiguration, }, }, }, }, - &mutating.PolicyBinding{ + &v1.MutatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: v1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy", }, }, @@ -265,7 +265,7 @@ func TestSSAMapList(t *testing.T) { }, } - testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator { + testContext := setupTest(t, func(p *v1.MutatingAdmissionPolicy) mutating.PolicyEvaluator { return mutating.PolicyEvaluator{ Mutators: []patch.Patcher{smdPatcher{patch: patchObj}}, } @@ -273,27 +273,27 @@ func TestSSAMapList(t *testing.T) { // Set up a policy and binding that match, no params require.NoError(t, testContext.UpdateAndWait( - &mutating.Policy{ + &v1.MutatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{Name: "policy"}, - Spec: v1beta1.MutatingAdmissionPolicySpec{ - MatchConstraints: &v1beta1.MatchResources{ - MatchPolicy: ptr.To(v1beta1.Equivalent), + Spec: v1.MutatingAdmissionPolicySpec{ + MatchConstraints: &v1.MatchResources{ + MatchPolicy: ptr.To(v1.Equivalent), NamespaceSelector: &metav1.LabelSelector{}, ObjectSelector: &metav1.LabelSelector{}, }, - Mutations: []v1beta1.Mutation{ + Mutations: []v1.Mutation{ { - ApplyConfiguration: &v1beta1.ApplyConfiguration{ + ApplyConfiguration: &v1.ApplyConfiguration{ Expression: "ignored, but required", }, - PatchType: v1beta1.PatchTypeApplyConfiguration, + PatchType: v1.PatchTypeApplyConfiguration, }, }, }, }, - &mutating.PolicyBinding{ + &v1.MutatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{Name: "binding"}, - Spec: v1beta1.MutatingAdmissionPolicyBindingSpec{ + Spec: v1.MutatingAdmissionPolicyBindingSpec{ PolicyName: "policy", }, }, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/reinvocationcontext.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/reinvocationcontext.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/reinvocationcontext.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/reinvocationcontext.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/mutating/reinvocationcontext_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/accessor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/accessor.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/accessor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/accessor.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/admission_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/admission_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/admission_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/admission_test.go index 04ade16d1..69ee612ec 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/admission_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/admission_test.go @@ -199,6 +199,8 @@ type validateFunc func( runtimeCELCostBudget int64, authz authorizer.Authorizer) validating.ValidateResult +func (f validateFunc) CompileError() error { return nil } + type fakeCompiler struct { ValidateFuncs map[types.NamespacedName]validating.Validator @@ -269,7 +271,7 @@ func (f *fakeMatcher) ValidateInitialization() error { return nil } -func (f *fakeMatcher) GetNamespace(name string) (*v1.Namespace, error) { +func (f *fakeMatcher) GetNamespace(ctx context.Context, name string) (*v1.Namespace, error) { return nil, nil } @@ -360,6 +362,7 @@ func setupTestCommon( matcher generic.PolicyMatcher, shouldStartInformers bool, ) *generic.PolicyTestContext[*validating.Policy, *validating.PolicyBinding, validating.Validator] { + t.Cleanup(generic.SetPolicyRefreshIntervalForTests(10 * time.Millisecond)) testContext, testContextCancel, err := generic.NewPolicyTestContext( t, validating.NewValidatingAdmissionPolicyAccessor, @@ -1508,7 +1511,6 @@ func TestParamRef(t *testing.T) { } t.Run(name, func(t *testing.T) { - t.Parallel() // Test creating a policy with a cluster or namesapce-scoped param // and binding with the provided configuration. Test will ensure // that the provided configuration is capable of matching diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/dispatcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/dispatcher.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/dispatcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/dispatcher.go index 8f3e22f64..0b5474b75 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/dispatcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/dispatcher.go @@ -189,7 +189,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm // if it is cluster scoped, namespaceName will be empty // Otherwise, get the Namespace resource. if namespaceName != "" { - namespace, err = c.matcher.GetNamespace(namespaceName) + namespace, err = c.matcher.GetNamespace(ctx, namespaceName) if err != nil { return err } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/initializer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/initializer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/initializer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/initializer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/interface.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/interface.go index 97eeb9550..fd8015e7c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/interface.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/interface.go @@ -92,4 +92,6 @@ type Validator interface { // Validate is used to take cel evaluations and convert into decisions // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult + // CompileError returns any error from CEL compilation, or nil if compilation succeeded. + CompileError() error } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/message.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/message.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/message.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/message.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/plugin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/plugin.go similarity index 75% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/plugin.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/plugin.go index 85db23cd8..348ecde2d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/plugin.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/plugin.go @@ -26,13 +26,14 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/admission/plugin/manifest/metrics" + "k8s.io/apiserver/pkg/admission/plugin/policy/config" "k8s.io/apiserver/pkg/admission/plugin/policy/generic" + "k8s.io/apiserver/pkg/admission/plugin/policy/manifest/source" "k8s.io/apiserver/pkg/admission/plugin/policy/matching" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/apiserver/pkg/features" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" @@ -45,38 +46,20 @@ const ( var ( lazyCompositionEnvTemplateWithStrictCostInit sync.Once - lazyCompositionEnvTemplateWithStrictCost *cel.CompositionEnv - - lazyCompositionEnvTemplateWithoutStrictCostInit sync.Once - lazyCompositionEnvTemplateWithoutStrictCost *cel.CompositionEnv + lazyCompositionEnvTemplateWithStrictCost *environment.EnvSet ) -func getCompositionEnvTemplateWithStrictCost() *cel.CompositionEnv { +func getCompositionEnvTemplateWithStrictCost() *environment.EnvSet { lazyCompositionEnvTemplateWithStrictCostInit.Do(func() { - env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) - if err != nil { - panic(err) - } - lazyCompositionEnvTemplateWithStrictCost = env + lazyCompositionEnvTemplateWithStrictCost = environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) }) return lazyCompositionEnvTemplateWithStrictCost } -func getCompositionEnvTemplateWithoutStrictCost() *cel.CompositionEnv { - lazyCompositionEnvTemplateWithoutStrictCostInit.Do(func() { - env, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), false)) - if err != nil { - panic(err) - } - lazyCompositionEnvTemplateWithoutStrictCost = env - }) - return lazyCompositionEnvTemplateWithoutStrictCost -} - // Register registers a plugin func Register(plugins *admission.Plugins) { plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) { - return NewPlugin(configFile), nil + return NewPlugin(configFile) }) } @@ -93,8 +76,42 @@ type Plugin struct { var _ admission.Interface = &Plugin{} var _ admission.ValidationInterface = &Plugin{} var _ initializer.WantsExcludedAdmissionResources = &Plugin{} +var _ initializer.WantsManifestLoaders = &Plugin{} + +// SetManifestLoaders provides the manifest load functions for scheme-based defaulting and validation. +func (a *Plugin) SetManifestLoaders(loaders *initializer.ManifestLoaders) { + if loaders == nil || loaders.LoadValidatingPolicyManifests == nil { + return + } + loadFunc := loaders.LoadValidatingPolicyManifests + a.SetStaticSourceFactory(func(manifestsDir string) (generic.ReloadableSource[PolicyHook], error) { + staticSource := source.NewStaticPolicySource(manifestsDir, a.GetAPIServerID(), + func(p *v1.ValidatingAdmissionPolicy) (Validator, error) { + v := compilePolicy(p) + if err := v.CompileError(); err != nil { + return nil, err + } + return v, nil + }, + func(dir string) ([]*v1.ValidatingAdmissionPolicy, []*v1.ValidatingAdmissionPolicyBinding, string, error) { + return loadFunc(dir) + }, + func(b *v1.ValidatingAdmissionPolicyBinding) string { return b.Spec.PolicyName }, + metrics.VAPManifestType, + ) + if err := staticSource.LoadInitial(); err != nil { + return nil, err + } + return staticSource, nil + }) +} + +func NewPlugin(configFile io.Reader) (*Plugin, error) { + cfg, err := config.LoadValidatingConfig(configFile) + if err != nil { + return nil, err + } -func NewPlugin(_ io.Reader) *Plugin { handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update) p := &Plugin{ @@ -118,7 +135,8 @@ func NewPlugin(_ io.Reader) *Plugin { ), } p.SetEnabled(true) - return p + p.SetStaticManifestsDir(cfg.StaticManifestsDir) + return p, nil } // Validate makes an admission decision based on the request attributes. @@ -131,19 +149,16 @@ func compilePolicy(policy *Policy) Validator { if policy.Spec.ParamKind != nil { hasParam = true } - strictCost := utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP) - optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true, StrictCost: strictCost} - expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false, StrictCost: strictCost} + optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true} + expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false} failurePolicy := policy.Spec.FailurePolicy var matcher matchconditions.Matcher = nil matchConditions := policy.Spec.MatchConditions - var compositionEnvTemplate *cel.CompositionEnv - if strictCost { - compositionEnvTemplate = getCompositionEnvTemplateWithStrictCost() - } else { - compositionEnvTemplate = getCompositionEnvTemplateWithoutStrictCost() + compositionEnvTemplate := getCompositionEnvTemplateWithStrictCost() + filterCompiler, err := cel.NewCompositedCompiler(compositionEnvTemplate) + if err != nil { + return NewValidator(nil, nil, nil, nil, failurePolicy, err) } - filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate) filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions) if len(matchConditions) > 0 { @@ -159,6 +174,7 @@ func compilePolicy(policy *Policy) Validator { filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions), filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions), failurePolicy, + nil, ) return res diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/policy_decision.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/policy_decision.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/policy_decision.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/policy_decision.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking.go index 192be9621..c8b258f16 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking.go @@ -25,7 +25,7 @@ import ( "github.com/google/cel-go/cel" - "k8s.io/api/admissionregistration/v1" + v1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -39,8 +39,6 @@ import ( "k8s.io/apiserver/pkg/cel/library" "k8s.io/apiserver/pkg/cel/openapi" "k8s.io/apiserver/pkg/cel/openapi/resolver" - "k8s.io/apiserver/pkg/features" - utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/klog/v2" ) @@ -179,20 +177,17 @@ func (c *TypeChecker) CreateContext(policy *v1.ValidatingAdmissionPolicy) *TypeC func (c *TypeChecker) compiler(ctx *TypeCheckingContext, typeOverwrite typeOverwrite) (*plugincel.CompositedCompiler, error) { envSet, err := buildEnvSet( - /* hasParams */ ctx.paramDeclType != nil, + /* hasParams */ !ctx.paramGVK.Empty(), /* hasAuthorizer */ true, typeOverwrite) if err != nil { return nil, err } - env, err := plugincel.NewCompositionEnv(plugincel.VariablesTypeName, envSet) + compiler, err := plugincel.NewCompositedCompilerForTypeChecking(envSet) if err != nil { return nil, err } - compiler := &plugincel.CompositedCompiler{ - Compiler: &typeCheckingCompiler{typeOverwrite: typeOverwrite, compositionEnv: env}, - CompositionEnv: env, - } + compiler.Compiler = &typeCheckingCompiler{typeOverwrite: typeOverwrite, compiler: compiler} return compiler, nil } @@ -210,9 +205,8 @@ func (c *TypeChecker) CheckExpression(ctx *TypeCheckingContext, expression strin continue } options := plugincel.OptionalVariableDeclarations{ - HasParams: ctx.paramDeclType != nil, + HasParams: !ctx.paramGVK.Empty(), HasAuthorizer: true, - StrictCost: utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP), } compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions) result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions) @@ -250,7 +244,11 @@ func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclT if err != nil { return nil, err } - return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil + declType := common.SchemaDeclType(&openapi.Schema{Schema: s}, true) + if declType == nil { + return nil, nil + } + return declType.MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil } func (c *TypeChecker) paramsGVK(policy *v1.ValidatingAdmissionPolicy) schema.GroupVersionKind { @@ -394,7 +392,7 @@ func (c *TypeChecker) tryRefreshRESTMapper() { } func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) { - baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForVAP)) + baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) requestType := plugincel.BuildRequestType() namespaceType := plugincel.BuildNamespaceType() @@ -410,12 +408,16 @@ func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*envi varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...) // object and oldObject, same type, type(s) resolved from constraints - declTypes = append(declTypes, types.object) + if types.object != nil { + declTypes = append(declTypes, types.object) + } varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...) // params, defined by ParamKind - if hasParams && types.params != nil { - declTypes = append(declTypes, types.params) + if hasParams { + if types.params != nil { + declTypes = append(declTypes, types.params) + } varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...) } @@ -452,8 +454,8 @@ func createVariableOpts(declType *apiservercel.DeclType, variables ...string) [] } type typeCheckingCompiler struct { - compositionEnv *plugincel.CompositionEnv - typeOverwrite typeOverwrite + compiler *plugincel.CompositedCompiler + typeOverwrite typeOverwrite } // CompileCELExpression compiles the given expression. @@ -472,7 +474,7 @@ func (c *typeCheckingCompiler) CompileCELExpression(expressionAccessor plugincel ExpressionAccessor: expressionAccessor, } } - env, err := c.compositionEnv.Env(mode) + env, err := c.compiler.Env(mode) if err != nil { return resultError(fmt.Sprintf("fail to build env: %v", err), apiservercel.ErrorTypeInternal) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking_test.go similarity index 87% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking_test.go index 4da14b929..63243f431 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/typechecking_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/typechecking_test.go @@ -214,6 +214,24 @@ func TestTypeCheck(t *testing.T) { }, }}, }} + noTypeSchemaPolicy := &v1.ValidatingAdmissionPolicy{Spec: v1.ValidatingAdmissionPolicySpec{ + Validations: []v1.Validation{ + { + Expression: "true", + }, + }, + MatchConstraints: &v1.MatchResources{ResourceRules: []v1.NamedRuleWithOperations{ + { + RuleWithOperations: v1.RuleWithOperations{ + Rule: v1.Rule{ + APIGroups: []string{"apps"}, + APIVersions: []string{"v1"}, + Resources: []string{"deployments"}, + }, + }, + }, + }}, + }} deploymentPolicyWithBadMessageExpression := deploymentPolicy.DeepCopy() deploymentPolicyWithBadMessageExpression.Spec.Validations[0].MessageExpression = "object.foo + 114514" // confusion @@ -297,6 +315,26 @@ func TestTypeCheck(t *testing.T) { }, }}, }} + + reproducerPolicy := &v1.ValidatingAdmissionPolicy{Spec: v1.ValidatingAdmissionPolicySpec{ + Validations: []v1.Validation{ + { + Expression: "has(object.spec.text)", + }, + }, + MatchConstraints: &v1.MatchResources{ResourceRules: []v1.NamedRuleWithOperations{ + { + RuleWithOperations: v1.RuleWithOperations{ + Rule: v1.Rule{ + APIGroups: []string{"example.com"}, + APIVersions: []string{"v1alpha1"}, + Resources: []string{"reproducers"}, + }, + }, + }, + }}, + }} + for _, tc := range []struct { name string schemaToReturn *spec.Schema @@ -375,6 +413,23 @@ func TestTypeCheck(t *testing.T) { toContain(`undefined field 'bar'`), }, }, + { + name: "params with untyped schema", + policy: &v1.ValidatingAdmissionPolicy{Spec: v1.ValidatingAdmissionPolicySpec{ + ParamKind: &v1.ParamKind{ + APIVersion: "v1", + Kind: "Config", + }, + Validations: []v1.Validation{ + { + Expression: "params != null", + }, + }, + MatchConstraints: deploymentPolicy.Spec.MatchConstraints, + }}, + schemaToReturn: &spec.Schema{}, + assertions: []assertionFunc{toBeEmpty}, + }, { name: "multiple expressions", policy: multiExpressionPolicy, @@ -437,6 +492,45 @@ func TestTypeCheck(t *testing.T) { toContain("found no matching overload for 'allowed' applied to 'kubernetes.authorization.Authorizer"), }, }, + { + name: "additionalProperties: true", + policy: reproducerPolicy, + schemaToReturn: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "spec": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "text": *spec.StringProperty(), + }, + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "problematicProperty": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{Allows: true}, + }, + }, + }, + }, + }, + }, + }, + }, + assertions: []assertionFunc{toBeEmpty}, + }, + { + name: "schema without type", + policy: noTypeSchemaPolicy, + schemaToReturn: &spec.Schema{}, + assertions: []assertionFunc{toBeEmpty}, + }, { name: "variables valid", policy: &v1.ValidatingAdmissionPolicy{Spec: v1.ValidatingAdmissionPolicySpec{ diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator.go index 4057e515e..2144795fd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator.go @@ -45,15 +45,19 @@ type validator struct { auditAnnotationFilter cel.ConditionEvaluator messageFilter cel.ConditionEvaluator failPolicy *v1.FailurePolicyType + // compileError holds any compilation error from the CEL expressions. + // If non-nil, the validator will return an error result based on the failPolicy. + compileError error } -func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator { +func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType, err error) Validator { return &validator{ celMatcher: celMatcher, validationFilter: validationFilter, auditAnnotationFilter: auditAnnotationFilter, messageFilter: messageFilter, failPolicy: failPolicy, + compileError: err, } } @@ -71,6 +75,10 @@ func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnota return AuditAnnotationActionError } +func (v *validator) CompileError() error { + return v.compileError +} + // Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. @@ -81,6 +89,17 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe } else { f = *v.failPolicy } + if v.compileError != nil { + return ValidateResult{ + Decisions: []PolicyDecision{ + { + Action: policyDecisionActionForError(f), + Evaluation: EvalError, + Message: v.compileError.Error(), + }, + }, + } + } if v.celMatcher != nil { matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams, authz) if matchResults.Error != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator_test.go index db75afd71..f6cc71d8e 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/policy/validating/validator_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/policy/validating/validator_test.go @@ -53,7 +53,7 @@ func (f *fakeCelFilter) ForInput(ctx context.Context, versionedAttr *admission.V if costBudget <= 0 { // this filter will cost 1, so cost = 0 means fail. return nil, -1, &apiservercel.Error{ Type: apiservercel.ErrorTypeInvalid, - Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"), + Detail: "validation failed due to running out of cost budget, no further validation rules will be run", Cause: apiservercel.ErrOutOfBudget, } } @@ -1035,7 +1035,7 @@ func TestContextCanceled(t *testing.T) { fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil) fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil) - fc := cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + fc := cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) f := fc.CompileCondition([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions) v := validator{ failPolicy: &fail, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission.go index 5455b414e..883ac4c62 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission.go @@ -26,7 +26,6 @@ import ( "k8s.io/apiserver/pkg/admission" genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" resourcequotaapi "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota" - v1 "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1" "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/validation" quota "k8s.io/apiserver/pkg/quota/v1" "k8s.io/apiserver/pkg/quota/v1/generic" @@ -38,7 +37,7 @@ import ( const PluginName = "ResourceQuota" var ( - namespaceGVK = v1.SchemeGroupVersion.WithKind("Namespace").GroupKind() + namespaceGVK = corev1.SchemeGroupVersion.WithKind("Namespace").GroupKind() stopChUnconfiguredErr = fmt.Errorf("quota configuration configured between stop channel") ) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission_test.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission_test.go index 0cc3d7be1..eac3a051c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/admission_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/admission_test.go @@ -25,7 +25,6 @@ import ( "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" - v1 "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1" "k8s.io/apiserver/pkg/quota/v1/generic" ) @@ -149,7 +148,7 @@ func TestExcludedOperations(t *testing.T) { }, { "namespace creation", - admission.NewAttributesRecord(nil, nil, v1.SchemeGroupVersion.WithKind("Namespace"), "namespace", "namespace", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil), + admission.NewAttributesRecord(nil, nil, corev1.SchemeGroupVersion.WithKind("Namespace"), "namespace", "namespace", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil), }, } for _, test := range testCases { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/install/install.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/install/install.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/validation/validation_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/apis/resourcequota/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/apis/resourcequota/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/config_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/config_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/controller.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/controller.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/controller.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/controller.go index 95c9c84f6..78022342d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/controller.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/controller.go @@ -65,7 +65,7 @@ type quotaEvaluator struct { workLock sync.Mutex work map[string][]*admissionWaiter dirtyWork map[string][]*admissionWaiter - inProgress sets.String + inProgress sets.Set[string] // controls the run method so that we can cleanly conform to the Evaluator interface workers int @@ -125,7 +125,7 @@ func NewQuotaEvaluator(quotaAccessor QuotaAccessor, ignoredResources map[schema. queue: workqueue.NewTypedWithConfig(workqueue.TypedQueueConfig[string]{Name: "admission_quota_controller"}), work: map[string][]*admissionWaiter{}, dirtyWork: map[string][]*admissionWaiter{}, - inProgress: sets.String{}, + inProgress: sets.Set[string]{}, workers: workers, stopCh: stopCh, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/resource_access.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/resource_access.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/resource_access.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/resource_access.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/resource_access_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/resource_access_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/resourcequota/resource_access_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/resourcequota/resource_access_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/accessors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/accessors.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/accessors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/accessors.go index 7ae29d5bb..f4e337302 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/accessors.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/accessors.go @@ -27,8 +27,6 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/apiserver/pkg/features" - utilfeature "k8s.io/apiserver/pkg/util/feature" webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/rest" ) @@ -141,16 +139,11 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompi Expression: matchCondition.Expression, } } - strictCost := false - if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { - strictCost = true - } m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition( expressions, cel.OptionalVariableDeclarations{ HasParams: false, HasAuthorizer: true, - StrictCost: strictCost, }, environment.StoredExpressions, ), m.FailurePolicy, "webhook", "admit", m.Name) @@ -274,16 +267,11 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCom Expression: matchCondition.Expression, } } - strictCost := false - if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { - strictCost = true - } v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition( expressions, cel.OptionalVariableDeclarations{ HasParams: false, HasAuthorizer: true, - StrictCost: strictCost, }, environment.StoredExpressions, ), v.FailurePolicy, "webhook", "validating", v.Name) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/accessors_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/accessors_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/accessors_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/accessors_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go similarity index 83% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go index b724c346a..0dea39459 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/install/install.go @@ -23,13 +23,10 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission" v1 "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1" - "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1" ) // Install registers the API group and adds types to a scheme func Install(scheme *runtime.Scheme) { utilruntime.Must(webhookadmission.AddToScheme(scheme)) utilruntime.Must(v1.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) - utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion)) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go similarity index 67% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go index 71ce47b1f..f6ba94e7c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/types.go @@ -26,4 +26,14 @@ type WebhookAdmission struct { // KubeConfigFile is the path to the kubeconfig file. KubeConfigFile string + + // StaticManifestsDir is the path to a directory containing static webhook + // configurations to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // ValidatingWebhookConfiguration and MutatingWebhookConfiguration + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go similarity index 65% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go index 632427d7d..be0920534 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/types.go @@ -26,4 +26,14 @@ type WebhookAdmission struct { // KubeConfigFile is the path to the kubeconfig file. KubeConfigFile string `json:"kubeConfigFile"` + + // StaticManifestsDir is the path to a directory containing static webhook + // configurations to be loaded at startup. Files with extensions .yaml, + // .yml, and .json are read. Only admissionregistration.k8s.io/v1 + // ValidatingWebhookConfiguration and MutatingWebhookConfiguration + // resources are supported. + // Using this field requires the ManifestBasedAdmissionControlConfig + // feature gate to be enabled. + // +optional + StaticManifestsDir string `json:"staticManifestsDir,omitempty"` } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go index 4cf69291b..90235e1e9 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.conversion.go @@ -49,6 +49,7 @@ func RegisterConversions(s *runtime.Scheme) error { func autoConvert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *WebhookAdmission, out *webhookadmission.WebhookAdmission, s conversion.Scope) error { out.KubeConfigFile = in.KubeConfigFile + out.StaticManifestsDir = in.StaticManifestsDir return nil } @@ -59,6 +60,7 @@ func Convert_v1_WebhookAdmission_To_webhookadmission_WebhookAdmission(in *Webhoo func autoConvert_webhookadmission_WebhookAdmission_To_v1_WebhookAdmission(in *webhookadmission.WebhookAdmission, out *WebhookAdmission, s conversion.Scope) error { out.KubeConfigFile = in.KubeConfigFile + out.StaticManifestsDir = in.StaticManifestsDir return nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig.go new file mode 100644 index 000000000..1b751effa --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig.go @@ -0,0 +1,73 @@ +/* +Copyright 2017 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" + "io" + "path" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/admission/plugin/manifest" + "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission" + v1 "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1" +) + +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict) +) + +func init() { + utilruntime.Must(webhookadmission.AddToScheme(scheme)) + utilruntime.Must(v1.AddToScheme(scheme)) +} + +// LoadConfig extracts the webhook admission configuration from configFile. +func LoadConfig(configFile io.Reader) (*webhookadmission.WebhookAdmission, error) { + if configFile == nil { + return &webhookadmission.WebhookAdmission{}, nil + } + // we have a config so parse it. + data, err := io.ReadAll(configFile) + if err != nil { + return nil, err + } + decoder := codecs.UniversalDecoder() + decodedObj, err := runtime.Decode(decoder, data) + if err != nil { + return nil, err + } + config, ok := decodedObj.(*webhookadmission.WebhookAdmission) + if !ok { + return nil, fmt.Errorf("unexpected type: %T", decodedObj) + } + + // KubeConfigFile may be empty when only staticManifestsDir is configured. + if len(config.KubeConfigFile) > 0 && !path.IsAbs(config.KubeConfigFile) { + return nil, field.Invalid(field.NewPath("kubeConfigFile"), config.KubeConfigFile, "must be an absolute file path") + } + + if err := manifest.ValidateStaticManifestsDir(config.StaticManifestsDir); err != nil { + return nil, err + } + + return config, nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go new file mode 100644 index 000000000..c4d3c49c3 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/config/kubeconfig_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2019 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 ( + "bytes" + "os" + "path" + "strings" + "testing" + + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func TestLoadConfig(t *testing.T) { + // Create a temp directory for tests that need a valid directory path + validDir := t.TempDir() + // Create a temp file for "not a directory" test + notADirFile := path.Join(t.TempDir(), "file.txt") + if err := os.WriteFile(notADirFile, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + testcases := []struct { + name string + input string + enableFeatureGate *bool + expectErr string + expectKubeconfig string + expectStaticManifestsDir string + }{ + { + name: "empty", + input: "", + expectErr: `'Kind' is missing in ''`, + }, + { + name: "unknown kind", + input: `{"kind":"Unknown","apiVersion":"v1"}`, + expectErr: `no kind "Unknown" is registered for version "v1"`, + }, + { + name: "valid v1", + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +`, + expectKubeconfig: "/foo", + }, + { + name: "valid v1 with staticManifestsDir", + enableFeatureGate: new(true), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +staticManifestsDir: ` + validDir + ` +`, + expectKubeconfig: "/foo", + expectStaticManifestsDir: validDir, + }, + { + name: "invalid relative staticManifestsDir", + enableFeatureGate: new(true), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +staticManifestsDir: relative/path +`, + expectErr: `staticManifestsDir: Invalid value: "relative/path": must be an absolute file path`, + }, + { + name: "staticManifestsDir forbidden when feature gate disabled", + enableFeatureGate: new(false), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +staticManifestsDir: /etc/kubernetes/admission +`, + expectErr: "staticManifestsDir: Forbidden", + }, + { + name: "staticManifestsDir must be a directory", + enableFeatureGate: new(true), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +staticManifestsDir: ` + notADirFile + ` +`, + expectErr: "must be a directory", + }, + { + name: "staticManifestsDir must exist", + enableFeatureGate: new(true), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +kubeConfigFile: /foo +staticManifestsDir: /nonexistent/path +`, + expectErr: "unable to read", + }, + { + name: "valid staticManifestsDir only (no kubeConfigFile)", + enableFeatureGate: new(true), + input: ` +kind: WebhookAdmissionConfiguration +apiVersion: apiserver.config.k8s.io/v1 +staticManifestsDir: ` + validDir + ` +`, + expectStaticManifestsDir: validDir, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + if tc.enableFeatureGate != nil { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ManifestBasedAdmissionControlConfig, *tc.enableFeatureGate) + } + cfg, err := LoadConfig(bytes.NewBufferString(tc.input)) + if len(tc.expectErr) > 0 { + if err == nil { + t.Fatal("expected err, got none") + } + if !strings.Contains(err.Error(), tc.expectErr) { + t.Fatalf("expected err containing %q, got %v", tc.expectErr, err) + } + return + } + if err != nil { + t.Fatal(err) + } + if cfg.KubeConfigFile != tc.expectKubeconfig { + t.Fatalf("expected KubeConfigFile %q, got %q", tc.expectKubeconfig, cfg.KubeConfigFile) + } + if cfg.StaticManifestsDir != tc.expectStaticManifestsDir { + t.Fatalf("expected StaticManifestsDir %q, got %q", tc.expectStaticManifestsDir, cfg.StaticManifestsDir) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/statuserror.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/statuserror.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/statuserror.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/statuserror.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/statuserror_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/statuserror_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/errors/statuserror_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/errors/statuserror_test.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source.go new file mode 100644 index 000000000..3b38c9fad --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source.go @@ -0,0 +1,99 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package generic + +import ( + "sync" + + "k8s.io/apiserver/pkg/admission/plugin/webhook" +) + +// compositeWebhookSource combines multiple webhook sources into a single source. +// Static (manifest-based) webhooks are returned before API-based webhooks. +type compositeWebhookSource struct { + staticSource Source + apiSource Source + + mu sync.RWMutex + lastStaticSlice []webhook.WebhookAccessor + lastAPISlice []webhook.WebhookAccessor + lastCombined []webhook.WebhookAccessor +} + +var _ Source = &compositeWebhookSource{} + +// NewCompositeWebhookSource creates a webhook source that combines static and API-based sources. +// Static webhooks are evaluated first, followed by API-based webhooks. +// If staticSource is nil, only apiSource webhooks are returned. +func NewCompositeWebhookSource(staticSource, apiSource Source) Source { + if staticSource == nil { + return apiSource + } + return &compositeWebhookSource{ + staticSource: staticSource, + apiSource: apiSource, + } +} + +// Webhooks returns all webhook accessors from both sources. +// Static webhooks come first, followed by API-based webhooks. +// The combined slice is cached and reused when the underlying slices haven't changed. +func (c *compositeWebhookSource) Webhooks() []webhook.WebhookAccessor { + var staticSlice, apiSlice []webhook.WebhookAccessor + if c.staticSource != nil { + staticSlice = c.staticSource.Webhooks() + } + if c.apiSource != nil { + apiSlice = c.apiSource.Webhooks() + } + + c.mu.RLock() + if slicesAreEqual(c.lastStaticSlice, staticSlice) && slicesAreEqual(c.lastAPISlice, apiSlice) { + combined := c.lastCombined + c.mu.RUnlock() + return combined + } + c.mu.RUnlock() + + c.mu.Lock() + defer c.mu.Unlock() + // Re-check under write lock. + if slicesAreEqual(c.lastStaticSlice, staticSlice) && slicesAreEqual(c.lastAPISlice, apiSlice) { + return c.lastCombined + } + + combined := make([]webhook.WebhookAccessor, 0, len(staticSlice)+len(apiSlice)) + combined = append(combined, staticSlice...) + combined = append(combined, apiSlice...) + + c.lastStaticSlice = staticSlice + c.lastAPISlice = apiSlice + c.lastCombined = combined + return combined +} + +// slicesAreEqual reports whether two slices share the same backing array and length. +func slicesAreEqual[T any](a, b []T) bool { + return len(a) == len(b) && (len(a) == 0 || &a[0] == &b[0]) +} + +// HasSynced returns true only when both sources have synced. +func (c *compositeWebhookSource) HasSynced() bool { + staticSynced := c.staticSource == nil || c.staticSource.HasSynced() + apiSynced := c.apiSource == nil || c.apiSource.HasSynced() + return staticSynced && apiSynced +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source_test.go new file mode 100644 index 000000000..56f2f3c7a --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/composite_webhook_source_test.go @@ -0,0 +1,187 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package generic + +import ( + "testing" + + v1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apiserver/pkg/admission/plugin/webhook" +) + +// mockSource implements Source for testing. +type mockSource struct { + webhooks []webhook.WebhookAccessor + hasSynced bool +} + +func (m *mockSource) Webhooks() []webhook.WebhookAccessor { + return m.webhooks +} + +func (m *mockSource) HasSynced() bool { + return m.hasSynced +} + +var _ Source = &mockSource{} + +func createTestValidatingWebhook(uid, name string) webhook.WebhookAccessor { + return webhook.NewValidatingWebhookAccessor(uid, name, &v1.ValidatingWebhook{ + Name: "test.webhook.io", + ClientConfig: v1.WebhookClientConfig{ + URL: new("https://example.com"), + }, + AdmissionReviewVersions: []string{"v1"}, + }) +} + +func TestCompositeWebhookSource_Webhooks(t *testing.T) { + staticWebhook := createTestValidatingWebhook("static-1", "static-config") + apiWebhook := createTestValidatingWebhook("api-1", "api-config") + + tests := []struct { + name string + staticSource Source + apiSource Source + wantUIDs []string + }{ + { + name: "only static source", + staticSource: &mockSource{webhooks: []webhook.WebhookAccessor{staticWebhook}, hasSynced: true}, + apiSource: &mockSource{webhooks: nil, hasSynced: true}, + wantUIDs: []string{"static-1"}, + }, + { + name: "only api source", + staticSource: &mockSource{webhooks: nil, hasSynced: true}, + apiSource: &mockSource{webhooks: []webhook.WebhookAccessor{apiWebhook}, hasSynced: true}, + wantUIDs: []string{"api-1"}, + }, + { + name: "both sources - static first", + staticSource: &mockSource{webhooks: []webhook.WebhookAccessor{staticWebhook}, hasSynced: true}, + apiSource: &mockSource{webhooks: []webhook.WebhookAccessor{apiWebhook}, hasSynced: true}, + wantUIDs: []string{"static-1", "api-1"}, + }, + { + name: "nil static source", + staticSource: nil, + apiSource: &mockSource{webhooks: []webhook.WebhookAccessor{apiWebhook}, hasSynced: true}, + wantUIDs: []string{"api-1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := NewCompositeWebhookSource(tt.staticSource, tt.apiSource) + webhooks := source.Webhooks() + + if len(webhooks) != len(tt.wantUIDs) { + t.Errorf("Webhooks() returned %d webhooks, want %d", len(webhooks), len(tt.wantUIDs)) + return + } + + for i, w := range webhooks { + if w.GetUID() != tt.wantUIDs[i] { + t.Errorf("Webhooks()[%d].GetUID() = %s, want %s", i, w.GetUID(), tt.wantUIDs[i]) + } + } + }) + } +} + +func TestCompositeWebhookSource_Caching(t *testing.T) { + staticWebhook := createTestValidatingWebhook("static-1", "static-config") + apiWebhook := createTestValidatingWebhook("api-1", "api-config") + + staticSource := &mockSource{webhooks: []webhook.WebhookAccessor{staticWebhook}, hasSynced: true} + apiSource := &mockSource{webhooks: []webhook.WebhookAccessor{apiWebhook}, hasSynced: true} + + source := NewCompositeWebhookSource(staticSource, apiSource) + + // First call should create the combined slice. + webhooks1 := source.Webhooks() + if len(webhooks1) != 2 { + t.Fatalf("Expected 2 webhooks, got %d", len(webhooks1)) + } + + // Second call with same underlying slices should return the cached combined slice. + webhooks2 := source.Webhooks() + if &webhooks1[0] != &webhooks2[0] { + t.Error("Expected cached slice to be returned when underlying slices haven't changed") + } + + // Changing the underlying slice should produce a new combined slice. + newStaticWebhook := createTestValidatingWebhook("static-2", "static-config-2") + staticSource.webhooks = []webhook.WebhookAccessor{newStaticWebhook} + webhooks3 := source.Webhooks() + if webhooks3[0].GetUID() != "static-2" { + t.Errorf("Expected first webhook UID to be static-2 after update, got %s", webhooks3[0].GetUID()) + } + if &webhooks2[0] == &webhooks3[0] { + t.Error("Expected new slice after underlying source changed") + } +} + +func TestCompositeWebhookSource_HasSynced(t *testing.T) { + tests := []struct { + name string + staticSource Source + apiSource Source + want bool + }{ + { + name: "both synced", + staticSource: &mockSource{hasSynced: true}, + apiSource: &mockSource{hasSynced: true}, + want: true, + }, + { + name: "static not synced", + staticSource: &mockSource{hasSynced: false}, + apiSource: &mockSource{hasSynced: true}, + want: false, + }, + { + name: "api not synced", + staticSource: &mockSource{hasSynced: true}, + apiSource: &mockSource{hasSynced: false}, + want: false, + }, + { + name: "neither synced", + staticSource: &mockSource{hasSynced: false}, + apiSource: &mockSource{hasSynced: false}, + want: false, + }, + { + name: "nil static source, api synced", + staticSource: nil, + apiSource: &mockSource{hasSynced: true}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := NewCompositeWebhookSource(tt.staticSource, tt.apiSource) + if got := source.HasSynced(); got != tt.want { + t.Errorf("HasSynced() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/webhook.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/webhook.go similarity index 59% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/webhook.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/webhook.go index 8db7d3ced..cef27b233 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/webhook.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/webhook.go @@ -22,7 +22,6 @@ import ( "io" "k8s.io/apiserver/pkg/cel/environment" - "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/klog/v2" @@ -41,8 +40,10 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/features" webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/informers" + coreinformers "k8s.io/client-go/informers/core/v1" clientset "k8s.io/client-go/kubernetes" ) @@ -50,28 +51,63 @@ import ( type Webhook struct { *admission.Handler - sourceFactory sourceFactory + // Factories for creating webhook sources. + apiSourceFactory sourceFactory + staticSourceFactory StaticSourceFactory + + // Configuration provided via admission config file and initializers. + staticManifestsDir string // path to static webhook manifest directory (optional) + apiServerID string // identity of this API server instance, used for metrics + + // hookSource is the webhook source used at admission time. When staticManifestsDir + // is configured, this is a composite source combining staticSource (loaded from disk) + // with apiSource (from the API). Otherwise, it points directly to apiSource. + hookSource Source + // apiSource provides webhook configurations from the Kubernetes API (informer-based). + apiSource Source + // staticSource holds a reference to only the static (manifest-based) webhook source. + // This can be used to route requests for excluded resources to static hooks only. + staticSource ReloadableSource + + // Admission-time dependencies. + namespaceInformer coreinformers.NamespaceInformer + clientManager *webhookutil.ClientManager + namespaceMatcher *namespace.Matcher + objectMatcher *object.Matcher + dispatcher Dispatcher + filterCompiler cel.ConditionCompiler + authorizer authorizer.Authorizer - hookSource Source - clientManager *webhookutil.ClientManager - namespaceMatcher *namespace.Matcher - objectMatcher *object.Matcher - dispatcher Dispatcher - filterCompiler cel.ConditionCompiler - authorizer authorizer.Authorizer + // Lifecycle. + stopCh <-chan struct{} } var ( _ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{} + _ genericadmissioninit.WantsDrainedNotification = &Webhook{} + _ genericadmissioninit.WantsAPIServerID = &Webhook{} _ admission.Interface = &Webhook{} ) type sourceFactory func(f informers.SharedInformerFactory) Source type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher +// ReloadableSource extends Source with a method to run a reload loop +// that watches for configuration changes and blocks until the context is canceled. +type ReloadableSource interface { + Source + // RunReloadLoop watches for configuration changes and reloads when detected. + // It blocks until ctx is canceled. + RunReloadLoop(ctx context.Context) +} + +// StaticSourceFactory creates a static webhook source from a manifest directory. +// The returned Source should have LoadInitial() already called. +type StaticSourceFactory func(manifestsDir string) (ReloadableSource, error) + // NewWebhook creates a new generic admission webhook. func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) { - kubeconfigFile, err := config.LoadConfig(configFile) + cfg, err := config.LoadConfig(configFile) if err != nil { return nil, err } @@ -87,7 +123,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory if err != nil { return nil, err } - authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile) + authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(cfg.KubeConfigFile) if err != nil { return nil, err } @@ -96,13 +132,14 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver()) return &Webhook{ - Handler: handler, - sourceFactory: sourceFactory, - clientManager: &cm, - namespaceMatcher: &namespace.Matcher{}, - objectMatcher: &object.Matcher{}, - dispatcher: dispatcherFactory(&cm), - filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))), + Handler: handler, + apiSourceFactory: sourceFactory, + staticManifestsDir: cfg.StaticManifestsDir, + clientManager: &cm, + namespaceMatcher: &namespace.Matcher{}, + objectMatcher: &object.Matcher{}, + dispatcher: dispatcherFactory(&cm), + filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())), }, nil } @@ -119,6 +156,29 @@ func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) { a.clientManager.SetServiceResolver(sr) } +// SetStaticSourceFactory sets the factory for creating static webhook sources. +// This should be called before SetExternalKubeInformerFactory. +func (a *Webhook) SetStaticSourceFactory(factory StaticSourceFactory) { + a.staticSourceFactory = factory +} + +// SetAPIServerID implements the WantsAPIServerID interface. +// The API server ID is used for metrics labeling and must be set before +// SetExternalKubeInformerFactory is called. +func (a *Webhook) SetAPIServerID(id string) { + a.apiServerID = id +} + +// GetAPIServerID returns the stored API server ID. +func (a *Webhook) GetAPIServerID() string { + return a.apiServerID +} + +// SetDrainedNotification implements the WantsDrainedNotification interface. +func (a *Webhook) SetDrainedNotification(stopCh <-chan struct{}) { + a.stopCh = stopCh +} + // SetExternalKubeClientSet implements the WantsExternalKubeInformerFactory interface. // It sets external ClientSet for admission plugins that need it func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) { @@ -129,10 +189,10 @@ func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) { func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { namespaceInformer := f.Core().V1().Namespaces() a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister() - a.hookSource = a.sourceFactory(f) - a.SetReadyFunc(func() bool { - return namespaceInformer.Informer().HasSynced() && a.hookSource.HasSynced() - }) + a.namespaceInformer = namespaceInformer + + // Create the API-based source (stored for later use in ValidateInitialization) + a.apiSource = a.apiSourceFactory(f) } func (a *Webhook) SetAuthorizer(authorizer authorizer.Authorizer) { @@ -140,8 +200,10 @@ func (a *Webhook) SetAuthorizer(authorizer authorizer.Authorizer) { } // ValidateInitialization implements the InitializationValidator interface. +// Static source creation happens here (after all initializers have run) because +// SetManifestLoaders may be called after SetExternalKubeInformerFactory. func (a *Webhook) ValidateInitialization() error { - if a.hookSource == nil { + if a.apiSource == nil { return fmt.Errorf("kubernetes client is not properly setup") } if err := a.namespaceMatcher.Validate(); err != nil { @@ -150,6 +212,46 @@ func (a *Webhook) ValidateInitialization() error { if err := a.clientManager.Validate(); err != nil { return fmt.Errorf("clientManager is not properly setup: %v", err) } + + // Guard: if static manifests dir is set but feature gate is off, return an error + if len(a.staticManifestsDir) > 0 && !utilfeature.DefaultFeatureGate.Enabled(features.ManifestBasedAdmissionControlConfig) { + return fmt.Errorf("static webhook manifests dir %q configured but %s feature gate is not enabled", a.staticManifestsDir, features.ManifestBasedAdmissionControlConfig) + } + + // Construct hookSource. The static source path (which starts goroutines) + // is guarded to avoid duplicate construction. The API-only path is a cheap + // pointer assignment that must reflect the latest apiSource. + if len(a.staticManifestsDir) > 0 { + if a.hookSource == nil { + if a.staticSourceFactory == nil { + return fmt.Errorf("static webhook manifests configured in %q but no static source factory is set", a.staticManifestsDir) + } + if a.stopCh == nil { + return fmt.Errorf("stopCh not set: WantsDrainedNotification must be called before ValidateInitialization") + } + staticSource, err := a.staticSourceFactory(a.staticManifestsDir) + if err != nil { + return fmt.Errorf("failed to load static webhook manifests from %q: %w", a.staticManifestsDir, err) + } + a.staticSource = staticSource + // Start the file watcher in a background goroutine, tied to server shutdown + staticCtx, staticCancel := context.WithCancel(context.Background()) + go func() { + defer staticCancel() + <-a.stopCh + }() + go staticSource.RunReloadLoop(staticCtx) + // Use composite source that combines static + API sources + a.hookSource = NewCompositeWebhookSource(staticSource, a.apiSource) + } + } else { + a.hookSource = a.apiSource + } + + a.SetReadyFunc(func() bool { + return a.namespaceInformer.Informer().HasSynced() && a.hookSource.HasSynced() + }) + return nil } @@ -254,6 +356,16 @@ func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { r // Dispatch is called by the downstream Validate or Admit methods. func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { if rules.IsExemptAdmissionConfigurationResource(attr) { + // Admission config resources are excluded from API-based webhooks to + // prevent circular dependencies. However, static (manifest-based) webhooks + // are safe to evaluate since they don't have self-referential concerns. + if a.staticSource != nil { + if !a.staticSource.HasSynced() { + return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request")) + } + hooks := a.staticSource.Webhooks() + return a.dispatcher.Dispatch(ctx, attr, o, hooks) + } return nil } if !a.WaitForReady() { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/webhook_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/webhook_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/generic/webhook_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/generic/webhook_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/initializer/initializer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/initializer/initializer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/initializer/initializer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/initializer/initializer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/initializer/initializer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/initializer/initializer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/initializer/initializer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/initializer/initializer_test.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader.go new file mode 100644 index 000000000..258e5b47d --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader.go @@ -0,0 +1,220 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package loader provides generic functionality to load webhook configurations +// from manifest files. It handles file reading, YAML/JSON decoding, and generic +// manifest validation. Type-specific defaulting and validation (e.g., scheme-based +// defaulting via internal types) are injected by callers through the AcceptObjectFunc +// callback. +package loader + +import ( + "fmt" + "sort" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission/plugin/manifest" + "k8s.io/apiserver/pkg/admission/plugin/webhook" +) + +// AcceptObjectFunc extracts typed items from a decoded runtime.Object, applying +// defaulting and validation. Returns the extracted items, or an error if the +// object type is not recognized or processing fails. +type AcceptObjectFunc[T metav1.Object] func(obj runtime.Object) ([]T, error) + +// LoadManifests loads webhook configurations from manifest files in dir using +// the provided decoder and acceptObject callback. It handles file I/O, YAML +// splitting, decoding, v1.List unwrapping, manifest name validation, webhook +// client config validation, and deterministic sorting by name. +func LoadManifests[T metav1.Object]( + dir string, + decoder runtime.Decoder, + acceptObject AcceptObjectFunc[T], +) ([]T, string, error) { + fileDocs, hash, err := manifest.LoadFiles(dir) + if err != nil { + return nil, "", err + } + + configs := make([]T, 0) + seenNames := map[string]string{} + + for _, fd := range fileDocs { + obj, _, err := decoder.Decode(fd.Doc, nil, nil) + if err != nil { + return nil, "", fmt.Errorf("error loading %s: %w", fd.FilePath, err) + } + + items, err := acceptFileObject(obj, fd.FilePath, seenNames, decoder, acceptObject) + if err != nil { + return nil, "", fmt.Errorf("error loading %s: %w", fd.FilePath, err) + } + configs = append(configs, items...) + } + + sort.Slice(configs, func(i, j int) bool { + return configs[i].GetName() < configs[j].GetName() + }) + + return configs, hash, nil +} + +// acceptFileObject handles type dispatch for a decoded object, including +// generic v1.List unwrapping, manifest name validation, item validation, +// and the default unsupported-type error. +func acceptFileObject[T metav1.Object]( + obj runtime.Object, + filePath string, + seenNames map[string]string, + decoder runtime.Decoder, + acceptObject AcceptObjectFunc[T], +) ([]T, error) { + // Handle generic v1.List by recursing into each item + if list, ok := obj.(*metav1.List); ok { + var allItems []T + for _, rawItem := range list.Items { + itemObj, _, err := decoder.Decode(rawItem.Raw, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to decode list item: %w", err) + } + items, err := acceptFileObject(itemObj, filePath, seenNames, decoder, acceptObject) + if err != nil { + return nil, err + } + allItems = append(allItems, items...) + } + return allItems, nil + } + + items, err := acceptObject(obj) + if err != nil { + return nil, err + } + for _, item := range items { + if err := validateAcceptedItem(item, filePath, seenNames); err != nil { + return nil, err + } + } + return items, nil +} + +// validateAcceptedItem runs manifest name validation and webhook client config validation. +func validateAcceptedItem[T metav1.Object](item T, filePath string, seenNames map[string]string) error { + name := item.GetName() + if err := manifest.ValidateManifestName(name, filePath, seenNames); err != nil { + return err + } + // Validate webhook client configs for webhook configuration types. + obj, ok := any(item).(runtime.Object) + if !ok { + return fmt.Errorf("type %T does not implement runtime.Object", item) + } + return validateWebhookClientConfigs(obj, name, filePath) +} + +// validateWebhookClientConfigs validates that all webhooks in a configuration +// use URL-based client config (service references are not supported for manifests). +func validateWebhookClientConfigs(obj runtime.Object, configName, filePath string) error { + switch c := obj.(type) { + case *admissionregistrationv1.ValidatingWebhookConfiguration: + for _, wh := range c.Webhooks { + if err := ValidateWebhookClientConfig(wh.Name, configName, filePath, wh.ClientConfig); err != nil { + return err + } + } + case *admissionregistrationv1.MutatingWebhookConfiguration: + for _, wh := range c.Webhooks { + if err := ValidateWebhookClientConfig(wh.Name, configName, filePath, wh.ClientConfig); err != nil { + return err + } + } + default: + return fmt.Errorf("unsupported webhook configuration type %T in file %q", obj, filePath) + } + return nil +} + +// ValidateWebhookClientConfig checks that a webhook uses URL-based client config +// (service references are not supported for static manifests). +func ValidateWebhookClientConfig(webhookName, configName, filePath string, cc admissionregistrationv1.WebhookClientConfig) error { + if cc.Service != nil { + return fmt.Errorf("webhook %q in %q (file %q): clientConfig.service is not supported for static manifests; use clientConfig.url instead", webhookName, configName, filePath) + } + if cc.URL == nil || len(*cc.URL) == 0 { + return fmt.Errorf("webhook %q in %q (file %q): clientConfig.url is required for static manifests", webhookName, configName, filePath) + } + return nil +} + +// ValidatingLoadResult holds the validating webhook configurations loaded from manifest files. +type ValidatingLoadResult struct { + // Configurations is the list of loaded validating webhook configurations. + Configurations []*admissionregistrationv1.ValidatingWebhookConfiguration + // Hash is the sha256 hash of all loaded files, used for change detection. + Hash string +} + +// MutatingLoadResult holds the mutating webhook configurations loaded from manifest files. +type MutatingLoadResult struct { + // Configurations is the list of loaded mutating webhook configurations. + Configurations []*admissionregistrationv1.MutatingWebhookConfiguration + // Hash is the sha256 hash of all loaded files, used for change detection. + Hash string +} + +// BuildValidatingAccessors builds webhook accessors from validating webhook configurations. +func BuildValidatingAccessors(configs []*admissionregistrationv1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor { + var accessors []webhook.WebhookAccessor + for _, config := range configs { + names := map[string]int{} + for i := range config.Webhooks { + w := &config.Webhooks[i] + n := w.Name + uid := fmt.Sprintf("manifest/%s/%s/%d", config.Name, n, names[n]) + names[n]++ + accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, config.Name, w)) + } + } + return accessors +} + +// BuildMutatingAccessors builds webhook accessors from mutating webhook configurations. +func BuildMutatingAccessors(configs []*admissionregistrationv1.MutatingWebhookConfiguration) []webhook.WebhookAccessor { + var accessors []webhook.WebhookAccessor + for _, config := range configs { + names := map[string]int{} + for i := range config.Webhooks { + w := &config.Webhooks[i] + n := w.Name + uid := fmt.Sprintf("manifest/%s/%s/%d", config.Name, n, names[n]) + names[n]++ + accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, config.Name, w)) + } + } + return accessors +} + +// GetWebhookAccessors returns webhook accessors for all validating webhooks. +func (r *ValidatingLoadResult) GetWebhookAccessors() []webhook.WebhookAccessor { + return BuildValidatingAccessors(r.Configurations) +} + +// GetWebhookAccessors returns webhook accessors for all mutating webhooks. +func (r *MutatingLoadResult) GetWebhookAccessors() []webhook.WebhookAccessor { + return BuildMutatingAccessors(r.Configurations) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader_test.go new file mode 100644 index 000000000..6bf4a6a8c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/loader/loader_test.go @@ -0,0 +1,264 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +func TestValidateWebhookClientConfig(t *testing.T) { + url := "https://example.com" + tests := []struct { + name string + cc admissionregistrationv1.WebhookClientConfig + wantErr bool + errContains string + }{ + { + name: "valid URL config", + cc: admissionregistrationv1.WebhookClientConfig{URL: &url}, + }, + { + name: "service ref", + cc: admissionregistrationv1.WebhookClientConfig{Service: &admissionregistrationv1.ServiceReference{Name: "svc", Namespace: "ns"}}, + wantErr: true, + errContains: "clientConfig.service is not supported", + }, + { + name: "no URL", + cc: admissionregistrationv1.WebhookClientConfig{}, + wantErr: true, + errContains: "clientConfig.url is required", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateWebhookClientConfig("wh", "cfg", "test.yaml", tt.cc) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestLoadManifests(t *testing.T) { + // Build a minimal decoder (admissionregistration types only, no full client-go scheme) + s := runtime.NewScheme() + utilruntime.Must(admissionregistrationv1.AddToScheme(s)) + s.AddUnversionedTypes(metav1.SchemeGroupVersion, &metav1.List{}, &metav1.Status{}) + decoder := serializer.NewCodecFactory(s).UniversalDeserializer() + + // Simple accept function that extracts the typed object (no kube-apiserver defaulting) + accept := func(obj runtime.Object) ([]*admissionregistrationv1.ValidatingWebhookConfiguration, error) { + vwc, ok := obj.(*admissionregistrationv1.ValidatingWebhookConfiguration) + if !ok { + return nil, fmt.Errorf("unsupported type %T", obj) + } + return []*admissionregistrationv1.ValidatingWebhookConfiguration{vwc}, nil + } + + tests := []struct { + name string + files map[string]string + wantConfigs int + wantErr bool + wantErrContain string + }{ + { + name: "single validating webhook", + files: map[string]string{ + "wh.yaml": `apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: test.static.k8s.io +webhooks: +- name: test.webhook.io + admissionReviewVersions: ["v1"] + clientConfig: + url: "https://example.com" + sideEffects: None +`, + }, + wantConfigs: 1, + }, + { + name: "empty directory", + files: map[string]string{}, + wantConfigs: 0, + }, + { + name: "unsupported resource type", + files: map[string]string{ + "wrong.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: not-a-webhook +`, + }, + wantErr: true, + wantErrContain: "error loading", + }, + { + name: "v1.List with validating webhook", + files: map[string]string{ + "list.yaml": `apiVersion: v1 +kind: List +items: +- apiVersion: admissionregistration.k8s.io/v1 + kind: ValidatingWebhookConfiguration + metadata: + name: v1list-wh.static.k8s.io + webhooks: + - name: list.webhook.io + admissionReviewVersions: ["v1"] + clientConfig: + url: "https://example.com" + sideEffects: None +`, + }, + wantConfigs: 1, + }, + { + name: "duplicate webhook config names", + files: map[string]string{ + "01-wh.yaml": `apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: dup.static.k8s.io +webhooks: +- name: first.webhook.io + admissionReviewVersions: ["v1"] + clientConfig: + url: "https://example.com" + sideEffects: None +`, + "02-wh.yaml": `apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: dup.static.k8s.io +webhooks: +- name: second.webhook.io + admissionReviewVersions: ["v1"] + clientConfig: + url: "https://example.com" + sideEffects: None +`, + }, + wantErr: true, + wantErrContain: "duplicate", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for name, content := range tt.files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil { + t.Fatalf("failed to write file %s: %v", name, err) + } + } + + configs, hash, err := LoadManifests(dir, decoder, accept) + + if tt.wantErr { + if err == nil { + t.Error("expected error but got none") + } else if tt.wantErrContain != "" && !strings.Contains(err.Error(), tt.wantErrContain) { + t.Errorf("expected error containing %q, got %q", tt.wantErrContain, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(configs) != tt.wantConfigs { + t.Errorf("expected %d configs, got %d", tt.wantConfigs, len(configs)) + } + if tt.wantConfigs > 0 && hash == "" { + t.Error("expected non-empty hash") + } + }) + } +} + +func TestValidatingLoadResult_GetWebhookAccessors(t *testing.T) { + url := "https://example.com" + result := &ValidatingLoadResult{ + Configurations: []*admissionregistrationv1.ValidatingWebhookConfiguration{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test.static.k8s.io"}, + Webhooks: []admissionregistrationv1.ValidatingWebhook{ + { + Name: "first.webhook.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: &url}, + AdmissionReviewVersions: []string{"v1"}, + }, + { + Name: "second.webhook.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: &url}, + AdmissionReviewVersions: []string{"v1"}, + }, + }, + }, + }, + } + accessors := result.GetWebhookAccessors() + if len(accessors) != 2 { + t.Fatalf("expected 2 accessors, got %d", len(accessors)) + } +} + +func TestMutatingLoadResult_GetWebhookAccessors(t *testing.T) { + url := "https://example.com" + result := &MutatingLoadResult{ + Configurations: []*admissionregistrationv1.MutatingWebhookConfiguration{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test.static.k8s.io"}, + Webhooks: []admissionregistrationv1.MutatingWebhook{ + { + Name: "mutate.webhook.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: &url}, + AdmissionReviewVersions: []string{"v1"}, + }, + }, + }, + }, + } + accessors := result.GetWebhookAccessors() + if len(accessors) != 1 { + t.Fatalf("expected 1 accessor, got %d", len(accessors)) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source.go new file mode 100644 index 000000000..7ebd2bd5c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source.go @@ -0,0 +1,218 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package source provides a Source implementation that loads webhook configurations from manifest files. +package source + +import ( + "context" + "errors" + "fmt" + "sync/atomic" + "time" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission/plugin/manifest/metrics" + "k8s.io/apiserver/pkg/admission/plugin/webhook" + stagingloader "k8s.io/apiserver/pkg/admission/plugin/webhook/manifest/loader" + "k8s.io/apiserver/pkg/util/filesystem" + "k8s.io/klog/v2" +) + +// defaultReloadInterval is the default interval at which the manifest directory is checked for changes. +var defaultReloadInterval = 1 * time.Minute + +// SetReloadIntervalForTests sets the reload interval for testing and returns a function to restore the original value. +func SetReloadIntervalForTests(interval time.Duration) func() { + original := defaultReloadInterval + defaultReloadInterval = interval + return func() { + defaultReloadInterval = original + } +} + +// ValidatingWebhookLoadFunc loads validating webhook configurations from a directory. +// Returns the configurations, a hash string for change detection, and any error. +type ValidatingWebhookLoadFunc func(dir string) ([]*admissionregistrationv1.ValidatingWebhookConfiguration, string, error) + +// MutatingWebhookLoadFunc loads mutating webhook configurations from a directory. +// Returns the configurations, a hash string for change detection, and any error. +type MutatingWebhookLoadFunc func(dir string) ([]*admissionregistrationv1.MutatingWebhookConfiguration, string, error) + +// accessorBuilder builds webhook accessors from configurations. +type accessorBuilder[T runtime.Object] func(configs []T) []webhook.WebhookAccessor + +// webhookData holds the currently loaded webhook configurations and pre-built accessors. +type webhookData[T runtime.Object] struct { + configurations []T + accessors []webhook.WebhookAccessor +} + +// staticWebhookSource provides webhook configurations loaded from manifest files. +type staticWebhookSource[T runtime.Object] struct { + manifestsDir string + apiServerID string + reloadInterval time.Duration + webhookType metrics.ManifestType + loadFunc func(dir string) ([]T, string, error) + buildAccessors accessorBuilder[T] + + current atomic.Pointer[webhookData[T]] + lastReadHash atomic.Pointer[string] // hash of last file content read (for short-circuiting) + hasSynced atomic.Bool +} + +// ValidatingSource provides validating webhook configurations loaded from manifest files. +type ValidatingSource struct { + *staticWebhookSource[*admissionregistrationv1.ValidatingWebhookConfiguration] +} + +// MutatingSource provides mutating webhook configurations loaded from manifest files. +type MutatingSource struct { + *staticWebhookSource[*admissionregistrationv1.MutatingWebhookConfiguration] +} + +// NewValidatingSource creates a new validating webhook source that loads configurations from the specified directory. +func NewValidatingSource(manifestsDir, apiServerID string, loadFunc ValidatingWebhookLoadFunc) *ValidatingSource { + metrics.RegisterMetrics() + return &ValidatingSource{ + staticWebhookSource: &staticWebhookSource[*admissionregistrationv1.ValidatingWebhookConfiguration]{ + manifestsDir: manifestsDir, + apiServerID: apiServerID, + reloadInterval: defaultReloadInterval, + webhookType: metrics.ValidatingWebhookManifestType, + loadFunc: loadFunc, + buildAccessors: stagingloader.BuildValidatingAccessors, + }, + } +} + +// NewMutatingSource creates a new mutating webhook source that loads configurations from the specified directory. +func NewMutatingSource(manifestsDir, apiServerID string, loadFunc MutatingWebhookLoadFunc) *MutatingSource { + metrics.RegisterMetrics() + return &MutatingSource{ + staticWebhookSource: &staticWebhookSource[*admissionregistrationv1.MutatingWebhookConfiguration]{ + manifestsDir: manifestsDir, + apiServerID: apiServerID, + reloadInterval: defaultReloadInterval, + webhookType: metrics.MutatingWebhookManifestType, + loadFunc: loadFunc, + buildAccessors: stagingloader.BuildMutatingAccessors, + }, + } +} + +// LoadInitial performs the initial load of webhook manifests. +func (s *staticWebhookSource[T]) LoadInitial() error { + configs, hash, err := s.loadFunc(s.manifestsDir) + if err != nil { + return err + } + + accessors := s.buildAccessors(configs) + + // Validate accessors eagerly to catch selector parse errors at startup. + if err := validateAccessors(accessors); err != nil { + return err + } + + s.current.Store(&webhookData[T]{configurations: configs, accessors: accessors}) + s.lastReadHash.Store(&hash) + s.hasSynced.Store(true) + + klog.InfoS("Loaded manifest-based webhook configurations", "plugin", string(s.webhookType), + "configurations", len(configs)) + metrics.RecordAutomaticReloadSuccess(s.webhookType, s.apiServerID, hash) + return nil +} + +// RunReloadLoop watches for configuration changes and reloads when detected. +// It blocks until ctx is canceled. +func (s *staticWebhookSource[T]) RunReloadLoop(ctx context.Context) { + filesystem.WatchUntil( + ctx, + s.reloadInterval, + s.manifestsDir, + func() { + s.checkAndReload() + }, + func(err error) { + klog.ErrorS(err, "watching manifest directory", "plugin", string(s.webhookType), "dir", s.manifestsDir) + }, + ) +} + +func (s *staticWebhookSource[T]) checkAndReload() { + configs, hash, err := s.loadFunc(s.manifestsDir) + if err != nil { + klog.ErrorS(err, "reloading admission manifest config", "plugin", string(s.webhookType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadFailure(s.webhookType, s.apiServerID) + return + } + + // Short-circuit if file content hasn't changed since last read. + if last := s.lastReadHash.Load(); last != nil && hash == *last { + return + } + s.lastReadHash.Store(&hash) + + // Build and validate accessors (configs may be empty if files deleted) + accessors := s.buildAccessors(configs) + if err := validateAccessors(accessors); err != nil { + klog.ErrorS(err, "manifest webhook accessor validation failed after reload", "plugin", string(s.webhookType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadFailure(s.webhookType, s.apiServerID) + return + } + + s.current.Store(&webhookData[T]{configurations: configs, accessors: accessors}) + klog.InfoS("reloaded admission manifest config", "plugin", string(s.webhookType), "dir", s.manifestsDir) + metrics.RecordAutomaticReloadSuccess(s.webhookType, s.apiServerID, hash) +} + +// HasSynced returns true if the initial load has completed. +func (s *staticWebhookSource[T]) HasSynced() bool { + return s.hasSynced.Load() +} + +// Webhooks returns the list of webhook accessors. +func (s *staticWebhookSource[T]) Webhooks() []webhook.WebhookAccessor { + current := s.current.Load() + if current == nil { + return nil + } + return current.accessors +} + +// validateAccessors exercises lazy getters on each accessor to surface latent +// errors (e.g. invalid label selectors) early. +// Note: We intentionally skip GetRESTClient validation here because it requires +// a ClientManager. API validation (applied during defaulting) already ensures +// CA bundles parse and URLs are present and valid, and the manifest loader +// rejects service references, which covers the inputs needed to construct a +// REST client at admission time. +func validateAccessors(accessors []webhook.WebhookAccessor) error { + var errs []error + for _, a := range accessors { + if _, err := a.GetParsedNamespaceSelector(); err != nil { + errs = append(errs, fmt.Errorf("webhook %q: invalid namespaceSelector: %w", a.GetName(), err)) + } + if _, err := a.GetParsedObjectSelector(); err != nil { + errs = append(errs, fmt.Errorf("webhook %q: invalid objectSelector: %w", a.GetName(), err)) + } + } + return errors.Join(errs...) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source_test.go new file mode 100644 index 000000000..bca5440a0 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/manifest/source/source_test.go @@ -0,0 +1,210 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "testing" + + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type mockLoadFunc struct { + configs []*admissionregistrationv1.ValidatingWebhookConfiguration + hash string + err error + callCount int +} + +func (m *mockLoadFunc) load(_ string) ([]*admissionregistrationv1.ValidatingWebhookConfiguration, string, error) { + m.callCount++ + return m.configs, m.hash, m.err +} + +func validVWC(name string) *admissionregistrationv1.ValidatingWebhookConfiguration { + return &admissionregistrationv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Webhooks: []admissionregistrationv1.ValidatingWebhook{{ + Name: "test.webhook.io", + ClientConfig: admissionregistrationv1.WebhookClientConfig{ + URL: new("https://example.com"), + }, + AdmissionReviewVersions: []string{"v1"}, + SideEffects: func() *admissionregistrationv1.SideEffectClass { + s := admissionregistrationv1.SideEffectClassNone + return &s + }(), + }}, + } +} + +func invalidSelectorVWC(name string) *admissionregistrationv1.ValidatingWebhookConfiguration { + vwc := validVWC(name) + vwc.Webhooks[0].NamespaceSelector = &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "key", + Operator: "InvalidOperator", + }}, + } + return vwc +} + +// TestValidatingSource_ValidateBeforeStore verifies that LoadInitial rejects +// configurations with invalid selectors and does not store them (Bug #1). +func TestValidatingSource_ValidateBeforeStore(t *testing.T) { + mock := &mockLoadFunc{ + configs: []*admissionregistrationv1.ValidatingWebhookConfiguration{invalidSelectorVWC("bad")}, + hash: "h1", + } + + src := NewValidatingSource("/tmp/test", "test-server", mock.load) + + if err := src.LoadInitial(); err == nil { + t.Fatal("LoadInitial should have returned an error for invalid selector") + } + + if webhooks := src.Webhooks(); webhooks != nil { + t.Errorf("Webhooks() = %v, want nil (bad config should not be stored)", webhooks) + } +} + +// TestValidatingSource_ReloadKeepsPreviousOnValidationFailure verifies that +// checkAndReload keeps the previous valid configuration when the new one fails +// validation (Bug #1 on reload path). +func TestValidatingSource_ReloadKeepsPreviousOnValidationFailure(t *testing.T) { + mock := &mockLoadFunc{ + configs: []*admissionregistrationv1.ValidatingWebhookConfiguration{validVWC("good")}, + hash: "h1", + } + + src := NewValidatingSource("/tmp/test", "test-server", mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial failed: %v", err) + } + + initialWebhooks := src.Webhooks() + if len(initialWebhooks) != 1 { + t.Fatalf("expected 1 webhook after LoadInitial, got %d", len(initialWebhooks)) + } + + // Switch to invalid config for reload + mock.configs = []*admissionregistrationv1.ValidatingWebhookConfiguration{invalidSelectorVWC("bad")} + mock.hash = "h2" + + src.checkAndReload() + + reloadedWebhooks := src.Webhooks() + if len(reloadedWebhooks) != 1 { + t.Fatalf("expected 1 webhook after failed reload, got %d", len(reloadedWebhooks)) + } + // Should still be the original accessor + if &initialWebhooks[0] != &reloadedWebhooks[0] { + t.Error("Webhooks() returned different slice after failed reload; previous config should be preserved") + } +} + +// TestValidatingSource_CachedAccessorSlice verifies that Webhooks() returns +// the same slice on repeated calls (Bug #2 — enables composite source caching). +func TestValidatingSource_CachedAccessorSlice(t *testing.T) { + mock := &mockLoadFunc{ + configs: []*admissionregistrationv1.ValidatingWebhookConfiguration{validVWC("cfg")}, + hash: "h1", + } + + src := NewValidatingSource("/tmp/test", "test-server", mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial failed: %v", err) + } + + result1 := src.Webhooks() + result2 := src.Webhooks() + + if len(result1) == 0 || len(result2) == 0 { + t.Fatal("expected non-empty webhook slices") + } + + if &result1[0] != &result2[0] { + t.Error("Webhooks() returned different slices; expected pointer-equal backing array for caching") + } +} + +// TestValidatingSource_PreviousHashSkipsReload verifies that when checkAndReload +// is called and the hash hasn't changed, the source keeps the original accessors +// (the source owns hash comparison). +func TestValidatingSource_PreviousHashSkipsReload(t *testing.T) { + mock := &mockLoadFunc{ + configs: []*admissionregistrationv1.ValidatingWebhookConfiguration{validVWC("cfg")}, + hash: "h1", + } + + src := NewValidatingSource("/tmp/test", "test-server", mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial failed: %v", err) + } + if mock.callCount != 1 { + t.Fatalf("expected 1 call after LoadInitial, got %d", mock.callCount) + } + + initialWebhooks := src.Webhooks() + + // loadFunc returns same hash — source should detect no change + src.checkAndReload() + + if mock.callCount != 2 { + t.Fatalf("expected 2 calls after checkAndReload, got %d", mock.callCount) + } + + afterReload := src.Webhooks() + if len(afterReload) != len(initialWebhooks) { + t.Fatalf("expected %d webhooks after no-op reload, got %d", len(initialWebhooks), len(afterReload)) + } + if &afterReload[0] != &initialWebhooks[0] { + t.Error("Webhooks() returned different slice after no-op reload; original should be preserved") + } +} + +// TestValidatingSource_FilesDeletionClearsConfigs verifies that when loadFunc +// returns empty configs with a new hash, stored configs are updated to empty. +func TestValidatingSource_FilesDeletionClearsConfigs(t *testing.T) { + mock := &mockLoadFunc{ + configs: []*admissionregistrationv1.ValidatingWebhookConfiguration{validVWC("cfg")}, + hash: "h1", + } + + src := NewValidatingSource("/tmp/test", "test-server", mock.load) + + if err := src.LoadInitial(); err != nil { + t.Fatalf("LoadInitial failed: %v", err) + } + if len(src.Webhooks()) != 1 { + t.Fatalf("expected 1 webhook after LoadInitial, got %d", len(src.Webhooks())) + } + + // Simulate files deleted: empty configs, new hash + mock.configs = []*admissionregistrationv1.ValidatingWebhookConfiguration{} + mock.hash = "h2" + + src.checkAndReload() + + webhooks := src.Webhooks() + if len(webhooks) != 0 { + t.Errorf("expected 0 webhooks after files deletion, got %d", len(webhooks)) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/matcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/matcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/matcher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/matchconditions/matcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/matchconditions/matcher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/dispatcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/dispatcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/dispatcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/dispatcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/dispatcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/dispatcher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/dispatcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/dispatcher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/plugin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/plugin.go similarity index 73% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/plugin.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/plugin.go index fb07a61c1..4ffd4c97d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/plugin.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/plugin.go @@ -22,7 +22,9 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/admission/plugin/webhook/manifest/source" ) const ( @@ -48,6 +50,22 @@ type Plugin struct { } var _ admission.MutationInterface = &Plugin{} +var _ initializer.WantsManifestLoaders = &Plugin{} + +// SetManifestLoaders provides the manifest load functions for scheme-based defaulting and validation. +func (a *Plugin) SetManifestLoaders(loaders *initializer.ManifestLoaders) { + if loaders == nil || loaders.LoadMutatingWebhookManifests == nil { + return + } + loadFunc := loaders.LoadMutatingWebhookManifests + a.Webhook.SetStaticSourceFactory(func(manifestsDir string) (generic.ReloadableSource, error) { + src := source.NewMutatingSource(manifestsDir, a.Webhook.GetAPIServerID(), source.MutatingWebhookLoadFunc(loadFunc)) + if err := src.LoadInitial(); err != nil { + return nil, err + } + return src, nil + }) +} // NewMutatingWebhook returns a generic admission webhook plugin. func NewMutatingWebhook(configFile io.Reader) (*Plugin, error) { @@ -58,7 +76,6 @@ func NewMutatingWebhook(configFile io.Reader) (*Plugin, error) { if err != nil { return nil, err } - return p, nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/plugin_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/plugin_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/plugin_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/plugin_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go similarity index 88% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go index de0af221e..d7237d051 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/mutating/reinvocationcontext.go @@ -27,9 +27,9 @@ type webhookReinvokeContext struct { lastWebhookOutput runtime.Object // previouslyInvokedReinvocableWebhooks holds the set of webhooks that have been invoked and // should be reinvoked if a later mutation occurs - previouslyInvokedReinvocableWebhooks sets.String + previouslyInvokedReinvocableWebhooks sets.Set[string] // reinvokeWebhooks holds the set of webhooks that should be reinvoked - reinvokeWebhooks sets.String + reinvokeWebhooks sets.Set[string] } func (rc *webhookReinvokeContext) ShouldReinvokeWebhook(webhook string) bool { @@ -50,7 +50,7 @@ func (rc *webhookReinvokeContext) SetLastWebhookInvocationOutput(object runtime. func (rc *webhookReinvokeContext) AddReinvocableWebhookToPreviouslyInvoked(webhook string) { if rc.previouslyInvokedReinvocableWebhooks == nil { - rc.previouslyInvokedReinvocableWebhooks = sets.NewString() + rc.previouslyInvokedReinvocableWebhooks = sets.New[string]() } rc.previouslyInvokedReinvocableWebhooks.Insert(webhook) } @@ -58,11 +58,11 @@ func (rc *webhookReinvokeContext) AddReinvocableWebhookToPreviouslyInvoked(webho func (rc *webhookReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() { if len(rc.previouslyInvokedReinvocableWebhooks) > 0 { if rc.reinvokeWebhooks == nil { - rc.reinvokeWebhooks = sets.NewString() + rc.reinvokeWebhooks = sets.New[string]() } for s := range rc.previouslyInvokedReinvocableWebhooks { rc.reinvokeWebhooks.Insert(s) } - rc.previouslyInvokedReinvocableWebhooks = sets.NewString() + rc.previouslyInvokedReinvocableWebhooks = sets.New[string]() } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go index 7817cb177..d73c69bba 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/matcher.go @@ -44,8 +44,13 @@ type Matcher struct { Client clientset.Interface } -func (m *Matcher) GetNamespace(name string) (*v1.Namespace, error) { - return m.NamespaceLister.Get(name) +func (m *Matcher) GetNamespace(ctx context.Context, name string) (*v1.Namespace, error) { + ns, err := m.NamespaceLister.Get(name) + if apierrors.IsNotFound(err) && len(name) > 0 { + // in case of latency in our caches, make a call direct to storage to verify that it truly exists or not + ns, err = m.Client.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) + } + return ns, err } // Validate checks if the Matcher has a NamespaceLister and Client. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/matcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/matcher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/namespace/matcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/namespace/matcher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/matcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/matcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/matcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/matcher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/object/matcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/object/matcher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/rules/rules.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/rules/rules.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/rules/rules.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/rules/rules.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go index a3604462b..0142afbcc 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/predicates/rules/rules_test.go @@ -396,11 +396,11 @@ func TestScope(t *testing.T) { noMatch: attrList(), }, } - keys := sets.NewString() + keys := sets.New[string]() for name := range table { keys.Insert(name) } - for _, name := range keys.List() { + for _, name := range sets.List(keys) { tt := table[name] for i, m := range tt.match { t.Run(fmt.Sprintf("%s_match_%d", name, i), func(t *testing.T) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/admissionreview.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/admissionreview.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/admissionreview.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/admissionreview.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/admissionreview_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/admissionreview_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/admissionreview_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/admissionreview_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/request/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/request/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/certs.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/certs.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/certs.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/certs.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/gencerts.sh b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/gencerts.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testcerts/gencerts.sh rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testcerts/gencerts.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/main/main.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/main/main.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/main/main.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/main/main.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/service_resolver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/service_resolver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/service_resolver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/service_resolver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/testcase.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/testcase.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/testcase.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/testcase.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/webhook_server.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/webhook_server.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/testing/webhook_server.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/testing/webhook_server.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/util/client_config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/util/client_config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/util/client_config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/util/client_config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/dispatcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/dispatcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/dispatcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/dispatcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/plugin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/plugin.go similarity index 72% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/plugin.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/plugin.go index 6972877b1..da2096021 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/plugin.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/plugin.go @@ -22,7 +22,9 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/configuration" + "k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" + "k8s.io/apiserver/pkg/admission/plugin/webhook/manifest/source" ) const ( @@ -48,6 +50,22 @@ type Plugin struct { } var _ admission.ValidationInterface = &Plugin{} +var _ initializer.WantsManifestLoaders = &Plugin{} + +// SetManifestLoaders provides the manifest load functions for scheme-based defaulting and validation. +func (a *Plugin) SetManifestLoaders(loaders *initializer.ManifestLoaders) { + if loaders == nil || loaders.LoadValidatingWebhookManifests == nil { + return + } + loadFunc := loaders.LoadValidatingWebhookManifests + a.Webhook.SetStaticSourceFactory(func(manifestsDir string) (generic.ReloadableSource, error) { + src := source.NewValidatingSource(manifestsDir, a.Webhook.GetAPIServerID(), source.ValidatingWebhookLoadFunc(loadFunc)) + if err := src.LoadInitial(); err != nil { + return nil, err + } + return src, nil + }) +} // NewValidatingAdmissionWebhook returns a generic admission webhook plugin. func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/plugin_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/plugin_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/validating/plugin_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugin/webhook/validating/plugin_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugins.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugins.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugins.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/plugins.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/reinvocation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/reinvocation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/reinvocation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/reinvocation.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/testing/helpers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/testing/helpers.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/testing/helpers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/testing/helpers.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/admission/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/admission/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/fuzzer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/fuzzer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/fuzzer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/fuzzer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2beta1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2beta1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2beta1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2beta1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2beta1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2beta1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apidiscovery/v2beta1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apidiscovery/v2beta1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/install/install.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/install/install.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/load/load.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/load/load.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/load/load.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/load/load.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/load/load_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/load/load_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/load/load_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/load/load_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/types.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/types.go index 596b1f140..0c5e6b548 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/types.go @@ -146,6 +146,10 @@ type TLSConfig struct { // Must be configured if TCPTransport.URL is prefixed with https:// // +optional ClientCert string + + // tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used. + // +optional + TLSServerName string } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/types_encryption.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/types_encryption.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/types_encryption.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/types_encryption.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/defaults_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/defaults_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/defaults_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/defaults_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/types_encryption.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/types_encryption.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/types_encryption.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/types_encryption.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/types.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/types.go index 6e0f86da8..1e20c697b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/types.go @@ -147,6 +147,10 @@ type TLSConfig struct { // Must be configured if TCPTransport.URL is prefixed with https:// // +optional ClientCert string `json:"clientCert,omitempty"` + + // tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used. + // +optional + TLSServerName string `json:"tlsServerName,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go index 9d7a8ce62..4ea029789 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go @@ -817,6 +817,7 @@ func autoConvert_v1alpha1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *a out.CABundle = in.CABundle out.ClientKey = in.ClientKey out.ClientCert = in.ClientCert + out.TLSServerName = in.TLSServerName return nil } @@ -829,6 +830,7 @@ func autoConvert_apiserver_TLSConfig_To_v1alpha1_TLSConfig(in *apiserver.TLSConf out.CABundle = in.CABundle out.ClientKey = in.ClientKey out.ClientCert = in.ClientCert + out.TLSServerName = in.TLSServerName return nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1alpha1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1alpha1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/types.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/types.go index 16260efb6..44bfbbd3c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/types.go @@ -118,6 +118,10 @@ type TLSConfig struct { // Must be configured if TCPTransport.URL is prefixed with https:// // +optional ClientCert string `json:"clientCert,omitempty"` + + // tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used. + // +optional + TLSServerName string `json:"tlsServerName,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go index 4f25bd60c..04323e208 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go @@ -753,6 +753,7 @@ func autoConvert_v1beta1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *ap out.CABundle = in.CABundle out.ClientKey = in.ClientKey out.ClientCert = in.ClientCert + out.TLSServerName = in.TLSServerName return nil } @@ -765,6 +766,7 @@ func autoConvert_apiserver_TLSConfig_To_v1beta1_TLSConfig(in *apiserver.TLSConfi out.CABundle = in.CABundle out.ClientKey = in.ClientKey out.ClientCert = in.ClientCert + out.TLSServerName = in.TLSServerName return nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/v1beta1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/v1beta1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation.go index c81faca78..a5eeaef60 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation.go @@ -79,7 +79,7 @@ func ValidateAuthenticationConfiguration(compiler authenticationcel.Compiler, c if c.Anonymous != nil { if !utilfeature.DefaultFeatureGate.Enabled(features.AnonymousAuthConfigurableEndpoints) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("anonymous"), "anonymous is not supported when AnonymousAuthConfigurableEnpoints feature gate is disabled")) + allErrs = append(allErrs, field.Forbidden(field.NewPath("anonymous"), "anonymous is not supported when AnonymousAuthConfigurableEndpoints feature gate is disabled")) } if !c.Anonymous.Enabled && len(c.Anonymous.Conditions) > 0 { allErrs = append(allErrs, field.Invalid(field.NewPath("anonymous", "conditions"), c.Anonymous.Conditions, "enabled should be set to true when conditions are defined")) @@ -763,7 +763,7 @@ func ValidateWebhookConfiguration(compiler authorizationcel.Compiler, fldPath *f allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile})) } - _, errs := compileMatchConditions(compiler, c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration)) + _, errs := compileMatchConditions(compiler, c.MatchConditions, fldPath) allErrs = append(allErrs, errs...) return allErrs @@ -772,15 +772,11 @@ func ValidateWebhookConfiguration(compiler authorizationcel.Compiler, fldPath *f // ValidateAndCompileMatchConditions validates a given webhook's matchConditions. // This is exported for use in authz package. func ValidateAndCompileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) { - return compileMatchConditions(compiler, matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration)) + return compileMatchConditions(compiler, matchConditions, nil) } -func compileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) { +func compileMatchConditions(compiler authorizationcel.Compiler, matchConditions []api.WebhookMatchCondition, fldPath *field.Path) (*authorizationcel.CELMatcher, field.ErrorList) { var allErrs field.ErrorList - // should fail when match conditions are used without feature enabled - if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled { - allErrs = append(allErrs, field.Invalid(fldPath.Child("matchConditions"), "", "not supported when StructuredAuthorizationConfiguration feature gate is disabled")) - } if len(matchConditions) > 64 { allErrs = append(allErrs, field.TooMany(fldPath.Child("matchConditions"), len(matchConditions), 64)) return nil, allErrs diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_encryption.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_encryption.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_encryption.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_encryption.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_encryption_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_encryption_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_encryption_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_encryption_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_test.go index 70b22998f..e0ad15521 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/validation/validation_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/validation/validation_test.go @@ -828,8 +828,10 @@ func TestValidateAuthenticationConfiguration(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfigurationEgressSelector, *tt.structuredAuthnEgressSelectorFeatureOverride) } if tt.gaOnly { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, "AllAlpha", false) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, "AllBeta", false) + featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{ + "AllAlpha": false, + "AllBeta": false, + }) } got := ValidateAuthenticationConfiguration(authenticationcel.NewDefaultCompiler(), tt.in, tt.disallowedIssuers).ToAggregate() if d := cmp.Diff(tt.want, errString(got)); d != "" { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/apiserver/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/apiserver/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/fuzzer/fuzzer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/fuzzer/fuzzer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/fuzzer/fuzzer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/fuzzer/fuzzer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/helpers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/helpers.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/helpers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/helpers.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/install/install.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/install/install.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/install/roundtrip_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/install/roundtrip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/install/roundtrip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/install/roundtrip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/types.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/types.go index 17a398ed8..6218ae315 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/types.go @@ -97,6 +97,10 @@ type Event struct { // Impersonated user information. // +optional ImpersonatedUser *authnv1.UserInfo + // AuthenticationMetadata contains details about how the request was authenticated. + // +optional + AuthenticationMetadata *AuthenticationMetadata + // Source IPs, from where the request originated and intermediate proxies. // The source IPs are listed from (in order): // 1. X-Forwarded-For request header IPs @@ -147,6 +151,13 @@ type Event struct { Annotations map[string]string } +type AuthenticationMetadata struct { + // ImpersonationConstraint is the verb associated with the constrained impersonation mode that was used to authorize + // the ImpersonatedUser associated with this audit event. It is only set when constrained impersonation was used. + // +optional + ImpersonationConstraint string +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // EventList is a list of audit Events. @@ -264,6 +275,7 @@ type PolicyRule struct { type GroupResources struct { // Group is the name of the API group that contains the resources. // The empty string represents the core API group. + // `*` matches all groups // +optional Group string // Resources is a list of resources this rule applies to. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/doc.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/doc.go index 6c719b847..7e4e08af1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/doc.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/doc.go @@ -19,6 +19,7 @@ limitations under the License. // +k8s:conversion-gen=k8s.io/apiserver/pkg/apis/audit // +k8s:openapi-gen=true // +k8s:defaulter-gen=TypeMeta +// +k8s:openapi-model-package=io.k8s.apiserver.pkg.apis.audit.v1 // +groupName=audit.k8s.io diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.pb.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.pb.go similarity index 82% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.pb.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.pb.go index 27dab8c09..9b6bb0a26 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.pb.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.pb.go @@ -23,14 +23,12 @@ import ( fmt "fmt" io "io" + "sort" - proto "github.com/gogo/protobuf/proto" - github_com_gogo_protobuf_sortkeys "github.com/gogo/protobuf/sortkeys" v1 "k8s.io/api/authentication/v1" v11 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" - math "math" math_bits "math/bits" reflect "reflect" strings "strings" @@ -38,310 +36,48 @@ import ( k8s_io_apimachinery_pkg_types "k8s.io/apimachinery/pkg/types" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf +func (m *AuthenticationMetadata) Reset() { *m = AuthenticationMetadata{} } -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +func (m *Event) Reset() { *m = Event{} } -func (m *Event) Reset() { *m = Event{} } -func (*Event) ProtoMessage() {} -func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{0} -} -func (m *Event) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *Event) XXX_Merge(src proto.Message) { - xxx_messageInfo_Event.Merge(m, src) -} -func (m *Event) XXX_Size() int { - return m.Size() -} -func (m *Event) XXX_DiscardUnknown() { - xxx_messageInfo_Event.DiscardUnknown(m) -} - -var xxx_messageInfo_Event proto.InternalMessageInfo - -func (m *EventList) Reset() { *m = EventList{} } -func (*EventList) ProtoMessage() {} -func (*EventList) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{1} -} -func (m *EventList) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *EventList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *EventList) XXX_Merge(src proto.Message) { - xxx_messageInfo_EventList.Merge(m, src) -} -func (m *EventList) XXX_Size() int { - return m.Size() -} -func (m *EventList) XXX_DiscardUnknown() { - xxx_messageInfo_EventList.DiscardUnknown(m) -} - -var xxx_messageInfo_EventList proto.InternalMessageInfo - -func (m *GroupResources) Reset() { *m = GroupResources{} } -func (*GroupResources) ProtoMessage() {} -func (*GroupResources) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{2} -} -func (m *GroupResources) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *GroupResources) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *GroupResources) XXX_Merge(src proto.Message) { - xxx_messageInfo_GroupResources.Merge(m, src) -} -func (m *GroupResources) XXX_Size() int { - return m.Size() -} -func (m *GroupResources) XXX_DiscardUnknown() { - xxx_messageInfo_GroupResources.DiscardUnknown(m) -} +func (m *EventList) Reset() { *m = EventList{} } -var xxx_messageInfo_GroupResources proto.InternalMessageInfo +func (m *GroupResources) Reset() { *m = GroupResources{} } -func (m *ObjectReference) Reset() { *m = ObjectReference{} } -func (*ObjectReference) ProtoMessage() {} -func (*ObjectReference) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{3} -} -func (m *ObjectReference) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *ObjectReference) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *ObjectReference) XXX_Merge(src proto.Message) { - xxx_messageInfo_ObjectReference.Merge(m, src) -} -func (m *ObjectReference) XXX_Size() int { - return m.Size() -} -func (m *ObjectReference) XXX_DiscardUnknown() { - xxx_messageInfo_ObjectReference.DiscardUnknown(m) -} +func (m *ObjectReference) Reset() { *m = ObjectReference{} } -var xxx_messageInfo_ObjectReference proto.InternalMessageInfo +func (m *Policy) Reset() { *m = Policy{} } -func (m *Policy) Reset() { *m = Policy{} } -func (*Policy) ProtoMessage() {} -func (*Policy) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{4} -} -func (m *Policy) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *Policy) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *Policy) XXX_Merge(src proto.Message) { - xxx_messageInfo_Policy.Merge(m, src) -} -func (m *Policy) XXX_Size() int { - return m.Size() -} -func (m *Policy) XXX_DiscardUnknown() { - xxx_messageInfo_Policy.DiscardUnknown(m) -} - -var xxx_messageInfo_Policy proto.InternalMessageInfo - -func (m *PolicyList) Reset() { *m = PolicyList{} } -func (*PolicyList) ProtoMessage() {} -func (*PolicyList) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{5} -} -func (m *PolicyList) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PolicyList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PolicyList) XXX_Merge(src proto.Message) { - xxx_messageInfo_PolicyList.Merge(m, src) -} -func (m *PolicyList) XXX_Size() int { - return m.Size() -} -func (m *PolicyList) XXX_DiscardUnknown() { - xxx_messageInfo_PolicyList.DiscardUnknown(m) -} +func (m *PolicyList) Reset() { *m = PolicyList{} } -var xxx_messageInfo_PolicyList proto.InternalMessageInfo +func (m *PolicyRule) Reset() { *m = PolicyRule{} } -func (m *PolicyRule) Reset() { *m = PolicyRule{} } -func (*PolicyRule) ProtoMessage() {} -func (*PolicyRule) Descriptor() ([]byte, []int) { - return fileDescriptor_62937bb89ca7b6dd, []int{6} -} -func (m *PolicyRule) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PolicyRule) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) +func (m *AuthenticationMetadata) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } - return b[:n], nil -} -func (m *PolicyRule) XXX_Merge(src proto.Message) { - xxx_messageInfo_PolicyRule.Merge(m, src) -} -func (m *PolicyRule) XXX_Size() int { - return m.Size() -} -func (m *PolicyRule) XXX_DiscardUnknown() { - xxx_messageInfo_PolicyRule.DiscardUnknown(m) -} - -var xxx_messageInfo_PolicyRule proto.InternalMessageInfo - -func init() { - proto.RegisterType((*Event)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.Event") - proto.RegisterMapType((map[string]string)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.Event.AnnotationsEntry") - proto.RegisterType((*EventList)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.EventList") - proto.RegisterType((*GroupResources)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.GroupResources") - proto.RegisterType((*ObjectReference)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.ObjectReference") - proto.RegisterType((*Policy)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.Policy") - proto.RegisterType((*PolicyList)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.PolicyList") - proto.RegisterType((*PolicyRule)(nil), "k8s.io.apiserver.pkg.apis.audit.v1.PolicyRule") + return dAtA[:n], nil } -func init() { - proto.RegisterFile("k8s.io/apiserver/pkg/apis/audit/v1/generated.proto", fileDescriptor_62937bb89ca7b6dd) +func (m *AuthenticationMetadata) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) } -var fileDescriptor_62937bb89ca7b6dd = []byte{ - // 1275 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0xcf, 0x6f, 0x1b, 0xd5, - 0x13, 0xcf, 0xc6, 0x71, 0x63, 0x8f, 0x1b, 0xc7, 0x79, 0xed, 0xf7, 0xdb, 0x25, 0x07, 0xdb, 0x18, - 0x09, 0x05, 0x08, 0xeb, 0xd6, 0x14, 0x5a, 0x55, 0x02, 0xc9, 0xa6, 0xa5, 0xb5, 0xd4, 0xa6, 0xd1, - 0x33, 0xee, 0x01, 0x71, 0xe8, 0x7a, 0x3d, 0xb5, 0x97, 0xd8, 0xbb, 0xdb, 0x7d, 0x6f, 0x8d, 0x72, - 0xe3, 0x1f, 0x40, 0xe2, 0xce, 0x7f, 0xc1, 0x0d, 0x71, 0xe2, 0x96, 0x63, 0x8f, 0x3d, 0x59, 0xc4, - 0xf0, 0x57, 0xe4, 0x80, 0xd0, 0x7b, 0xfb, 0xf6, 0x87, 0x9d, 0x58, 0x71, 0x38, 0x70, 0xf3, 0x9b, - 0xf9, 0x7c, 0x3e, 0x33, 0x3b, 0x3b, 0x33, 0x6f, 0x0d, 0x8d, 0xa3, 0xfb, 0xcc, 0xb0, 0xdd, 0xba, - 0xe9, 0xd9, 0x0c, 0xfd, 0x09, 0xfa, 0x75, 0xef, 0x68, 0x20, 0x4f, 0x75, 0x33, 0xe8, 0xdb, 0xbc, - 0x3e, 0xb9, 0x53, 0x1f, 0xa0, 0x83, 0xbe, 0xc9, 0xb1, 0x6f, 0x78, 0xbe, 0xcb, 0x5d, 0x52, 0x0b, - 0x39, 0x46, 0xcc, 0x31, 0xbc, 0xa3, 0x81, 0x3c, 0x19, 0x92, 0x63, 0x4c, 0xee, 0xec, 0x7e, 0x3c, - 0xb0, 0xf9, 0x30, 0xe8, 0x19, 0x96, 0x3b, 0xae, 0x0f, 0xdc, 0x81, 0x5b, 0x97, 0xd4, 0x5e, 0xf0, - 0x4a, 0x9e, 0xe4, 0x41, 0xfe, 0x0a, 0x25, 0x77, 0xf7, 0x93, 0x34, 0xea, 0x66, 0xc0, 0x87, 0xe8, - 0x70, 0xdb, 0x32, 0xb9, 0xed, 0x3a, 0x17, 0x24, 0xb0, 0x7b, 0x37, 0x41, 0x8f, 0x4d, 0x6b, 0x68, - 0x3b, 0xe8, 0x1f, 0x27, 0x79, 0x8f, 0x91, 0x9b, 0x17, 0xb1, 0xea, 0xcb, 0x58, 0x7e, 0xe0, 0x70, - 0x7b, 0x8c, 0xe7, 0x08, 0x9f, 0x5d, 0x46, 0x60, 0xd6, 0x10, 0xc7, 0xe6, 0x22, 0xaf, 0xf6, 0x17, - 0x40, 0xf6, 0xd1, 0x04, 0x1d, 0x4e, 0xf6, 0x21, 0x3b, 0xc2, 0x09, 0x8e, 0x74, 0xad, 0xaa, 0xed, - 0xe5, 0x5b, 0xff, 0x3f, 0x99, 0x56, 0xd6, 0x66, 0xd3, 0x4a, 0xf6, 0xa9, 0x30, 0x9e, 0x45, 0x3f, - 0x68, 0x08, 0x22, 0x07, 0xb0, 0x29, 0xeb, 0xd7, 0x7e, 0xa8, 0xaf, 0x4b, 0xfc, 0x5d, 0x85, 0xdf, - 0x6c, 0x86, 0xe6, 0xb3, 0x69, 0xe5, 0xdd, 0x65, 0x39, 0xf1, 0x63, 0x0f, 0x99, 0xd1, 0x6d, 0x3f, - 0xa4, 0x91, 0x88, 0x88, 0xce, 0xb8, 0x39, 0x40, 0x3d, 0x33, 0x1f, 0xbd, 0x23, 0x8c, 0x67, 0xd1, - 0x0f, 0x1a, 0x82, 0x48, 0x03, 0xc0, 0xc7, 0xd7, 0x01, 0x32, 0xde, 0xa5, 0x6d, 0x7d, 0x43, 0x52, - 0x88, 0xa2, 0x00, 0x8d, 0x3d, 0x34, 0x85, 0x22, 0x55, 0xd8, 0x98, 0xa0, 0xdf, 0xd3, 0xb3, 0x12, - 0x7d, 0x5d, 0xa1, 0x37, 0x5e, 0xa0, 0xdf, 0xa3, 0xd2, 0x43, 0x9e, 0xc0, 0x46, 0xc0, 0xd0, 0xd7, - 0xaf, 0x55, 0xb5, 0xbd, 0x42, 0xe3, 0x7d, 0x23, 0x69, 0x1d, 0x63, 0xfe, 0x3d, 0x1b, 0x93, 0x3b, - 0x46, 0x97, 0xa1, 0xdf, 0x76, 0x5e, 0xb9, 0x89, 0x92, 0xb0, 0x50, 0xa9, 0x40, 0x86, 0x50, 0xb2, - 0xc7, 0x1e, 0xfa, 0xcc, 0x75, 0x44, 0xad, 0x85, 0x47, 0xdf, 0xbc, 0x92, 0xea, 0xcd, 0xd9, 0xb4, - 0x52, 0x6a, 0x2f, 0x68, 0xd0, 0x73, 0xaa, 0xe4, 0x23, 0xc8, 0x33, 0x37, 0xf0, 0x2d, 0x6c, 0x1f, - 0x32, 0x3d, 0x57, 0xcd, 0xec, 0xe5, 0x5b, 0x5b, 0xb3, 0x69, 0x25, 0xdf, 0x89, 0x8c, 0x34, 0xf1, - 0x93, 0x3a, 0xe4, 0x45, 0x7a, 0xcd, 0x01, 0x3a, 0x5c, 0x2f, 0xc9, 0x3a, 0xec, 0xa8, 0xec, 0xf3, - 0xdd, 0xc8, 0x41, 0x13, 0x0c, 0x79, 0x09, 0x79, 0xb7, 0xf7, 0x1d, 0x5a, 0x9c, 0xe2, 0x2b, 0x3d, - 0x2f, 0x1f, 0xe0, 0x13, 0xe3, 0xf2, 0x89, 0x32, 0x9e, 0x47, 0x24, 0xf4, 0xd1, 0xb1, 0x30, 0x4c, - 0x29, 0x36, 0xd2, 0x44, 0x94, 0x0c, 0xa1, 0xe8, 0x23, 0xf3, 0x5c, 0x87, 0x61, 0x87, 0x9b, 0x3c, - 0x60, 0x3a, 0xc8, 0x30, 0xfb, 0xa9, 0x30, 0x71, 0xf3, 0x24, 0x91, 0xc4, 0xdc, 0x88, 0x40, 0x21, - 0xa7, 0x45, 0x66, 0xd3, 0x4a, 0x91, 0xce, 0xe9, 0xd0, 0x05, 0x5d, 0x62, 0xc2, 0x96, 0xea, 0x86, - 0x30, 0x11, 0xbd, 0x20, 0x03, 0xed, 0x2d, 0x0d, 0xa4, 0x26, 0xc7, 0xe8, 0x3a, 0x47, 0x8e, 0xfb, - 0xbd, 0xd3, 0xda, 0x99, 0x4d, 0x2b, 0x5b, 0x34, 0x2d, 0x41, 0xe7, 0x15, 0x49, 0x3f, 0x79, 0x18, - 0x15, 0xe3, 0xfa, 0x15, 0x63, 0xcc, 0x3d, 0x88, 0x0a, 0xb2, 0xa0, 0x49, 0x7e, 0xd4, 0x40, 0x57, - 0x71, 0x29, 0x5a, 0x68, 0x4f, 0xb0, 0xff, 0xb5, 0x3d, 0x46, 0xc6, 0xcd, 0xb1, 0xa7, 0x6f, 0xc9, - 0x80, 0xf5, 0xd5, 0xaa, 0xf7, 0xcc, 0xb6, 0x7c, 0x57, 0x70, 0x5b, 0x55, 0xd5, 0x06, 0x3a, 0x5d, - 0x22, 0x4c, 0x97, 0x86, 0x24, 0x2e, 0x14, 0xe5, 0x54, 0x26, 0x49, 0x14, 0xff, 0x5d, 0x12, 0xd1, - 0xd0, 0x17, 0x3b, 0x73, 0x72, 0x74, 0x41, 0x9e, 0xbc, 0x86, 0x82, 0xe9, 0x38, 0x2e, 0x97, 0x53, - 0xc3, 0xf4, 0xed, 0x6a, 0x66, 0xaf, 0xd0, 0x78, 0xb0, 0x4a, 0x5f, 0xca, 0x4d, 0x67, 0x34, 0x13, - 0xf2, 0x23, 0x87, 0xfb, 0xc7, 0xad, 0x1b, 0x2a, 0x70, 0x21, 0xe5, 0xa1, 0xe9, 0x18, 0xbb, 0x5f, - 0x40, 0x69, 0x91, 0x45, 0x4a, 0x90, 0x39, 0xc2, 0xe3, 0x70, 0x5d, 0x52, 0xf1, 0x93, 0xdc, 0x84, - 0xec, 0xc4, 0x1c, 0x05, 0x18, 0xae, 0x44, 0x1a, 0x1e, 0x1e, 0xac, 0xdf, 0xd7, 0x6a, 0xbf, 0x6a, - 0x90, 0x97, 0xc1, 0x9f, 0xda, 0x8c, 0x93, 0x6f, 0x21, 0x27, 0x9e, 0xbe, 0x6f, 0x72, 0x53, 0xd2, - 0x0b, 0x0d, 0x63, 0xb5, 0x5a, 0x09, 0xf6, 0x33, 0xe4, 0x66, 0xab, 0xa4, 0x32, 0xce, 0x45, 0x16, - 0x1a, 0x2b, 0x92, 0x03, 0xc8, 0xda, 0x1c, 0xc7, 0x4c, 0x5f, 0x97, 0x85, 0xf9, 0x60, 0xe5, 0xc2, - 0xb4, 0xb6, 0xa2, 0xad, 0xdb, 0x16, 0x7c, 0x1a, 0xca, 0xd4, 0x7e, 0xd6, 0xa0, 0xf8, 0xd8, 0x77, - 0x03, 0x8f, 0x62, 0xb8, 0x4a, 0x18, 0x79, 0x0f, 0xb2, 0x03, 0x61, 0x51, 0x77, 0x45, 0xcc, 0x0b, - 0x61, 0xa1, 0x4f, 0xac, 0x26, 0x3f, 0x62, 0xc8, 0x5c, 0xd4, 0x6a, 0x8a, 0x65, 0x68, 0xe2, 0x27, - 0xf7, 0xc4, 0x74, 0x86, 0x87, 0x03, 0x73, 0x8c, 0x4c, 0xcf, 0x48, 0x82, 0x9a, 0xb9, 0x94, 0x83, - 0xce, 0xe3, 0x6a, 0xbf, 0x64, 0x60, 0x7b, 0x61, 0xdd, 0x90, 0x7d, 0xc8, 0x45, 0x20, 0x95, 0x61, - 0x5c, 0xaf, 0x48, 0x8b, 0xc6, 0x08, 0xb1, 0x15, 0x1d, 0x21, 0xe5, 0x99, 0x96, 0x7a, 0x73, 0xc9, - 0x56, 0x3c, 0x88, 0x1c, 0x34, 0xc1, 0x88, 0x9b, 0x44, 0x1c, 0xd4, 0x55, 0x15, 0xef, 0x7f, 0x81, - 0xa5, 0xd2, 0x43, 0x5a, 0x90, 0x09, 0xec, 0xbe, 0xba, 0x98, 0x6e, 0x2b, 0x40, 0xa6, 0xbb, 0xea, - 0xad, 0x28, 0xc8, 0xe2, 0x21, 0x4c, 0xcf, 0x96, 0x15, 0x55, 0x77, 0x56, 0xfc, 0x10, 0xcd, 0xc3, - 0x76, 0x58, 0xe9, 0x18, 0x21, 0x6e, 0x44, 0xd3, 0xb3, 0x5f, 0xa0, 0xcf, 0x6c, 0xd7, 0x91, 0x37, - 0x58, 0xea, 0x46, 0x6c, 0x1e, 0xb6, 0x95, 0x87, 0xa6, 0x50, 0xa4, 0x09, 0xdb, 0x51, 0x11, 0x22, - 0xe2, 0xa6, 0x24, 0xde, 0x52, 0xc4, 0x6d, 0x3a, 0xef, 0xa6, 0x8b, 0x78, 0xf2, 0x29, 0x14, 0x58, - 0xd0, 0x8b, 0x8b, 0x9d, 0x93, 0xf4, 0x78, 0x9c, 0x3a, 0x89, 0x8b, 0xa6, 0x71, 0xb5, 0xdf, 0xd7, - 0xe1, 0xda, 0xa1, 0x3b, 0xb2, 0xad, 0x63, 0xf2, 0xf2, 0xdc, 0x2c, 0xdc, 0x5e, 0x6d, 0x16, 0xc2, - 0x97, 0x2e, 0xa7, 0x21, 0x7e, 0xd0, 0xc4, 0x96, 0x9a, 0x87, 0x0e, 0x64, 0xfd, 0x60, 0x84, 0xd1, - 0x3c, 0x18, 0xab, 0xcc, 0x43, 0x98, 0x1c, 0x0d, 0x46, 0x98, 0x34, 0xb7, 0x38, 0x31, 0x1a, 0x6a, - 0x91, 0x7b, 0x00, 0xee, 0xd8, 0xe6, 0x72, 0x53, 0x45, 0xcd, 0x7a, 0x4b, 0xa6, 0x10, 0x5b, 0x93, - 0xaf, 0x96, 0x14, 0x94, 0x3c, 0x86, 0x1d, 0x71, 0x7a, 0x66, 0x3a, 0xe6, 0x00, 0xfb, 0x5f, 0xd9, - 0x38, 0xea, 0x33, 0xd9, 0x28, 0xb9, 0xd6, 0x3b, 0x2a, 0xd2, 0xce, 0xf3, 0x45, 0x00, 0x3d, 0xcf, - 0xa9, 0xfd, 0xa6, 0x01, 0x84, 0x69, 0xfe, 0x07, 0x3b, 0xe5, 0xf9, 0xfc, 0x4e, 0xf9, 0x70, 0xf5, - 0x1a, 0x2e, 0x59, 0x2a, 0x7f, 0x67, 0xa2, 0xec, 0x45, 0x59, 0xaf, 0xf8, 0xf1, 0x59, 0x81, 0xac, - 0xf8, 0x46, 0x89, 0xb6, 0x4a, 0x5e, 0x20, 0xc5, 0xf7, 0x0b, 0xa3, 0xa1, 0x9d, 0x18, 0x00, 0xe2, - 0x87, 0x1c, 0x8d, 0xe8, 0xed, 0x14, 0xc5, 0xdb, 0xe9, 0xc6, 0x56, 0x9a, 0x42, 0x08, 0x41, 0xf1, - 0x05, 0x28, 0x5e, 0x44, 0x2c, 0x28, 0x3e, 0x0c, 0x19, 0x0d, 0xed, 0xc4, 0x4a, 0xef, 0xb2, 0xac, - 0xac, 0x41, 0x63, 0x95, 0x1a, 0xcc, 0xef, 0xcd, 0x64, 0xaf, 0x5c, 0xb8, 0x03, 0x0d, 0x80, 0x78, - 0xc9, 0x30, 0xfd, 0x5a, 0x92, 0x75, 0xbc, 0x85, 0x18, 0x4d, 0x21, 0xc8, 0xe7, 0xb0, 0xed, 0xb8, - 0x4e, 0x24, 0xd5, 0xa5, 0x4f, 0x99, 0xbe, 0x29, 0x49, 0x37, 0xc4, 0xec, 0x1e, 0xcc, 0xbb, 0xe8, - 0x22, 0x76, 0xa1, 0x85, 0x73, 0xab, 0xb7, 0xf0, 0x97, 0x17, 0xb5, 0x70, 0x5e, 0xb6, 0xf0, 0xff, - 0x56, 0x6d, 0xdf, 0xd6, 0x93, 0x93, 0xd3, 0xf2, 0xda, 0x9b, 0xd3, 0xf2, 0xda, 0xdb, 0xd3, 0xf2, - 0xda, 0x0f, 0xb3, 0xb2, 0x76, 0x32, 0x2b, 0x6b, 0x6f, 0x66, 0x65, 0xed, 0xed, 0xac, 0xac, 0xfd, - 0x31, 0x2b, 0x6b, 0x3f, 0xfd, 0x59, 0x5e, 0xfb, 0xa6, 0x76, 0xf9, 0x5f, 0xbe, 0x7f, 0x02, 0x00, - 0x00, 0xff, 0xff, 0x81, 0x06, 0x4f, 0x58, 0x17, 0x0e, 0x00, 0x00, +func (m *AuthenticationMetadata) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + i -= len(m.ImpersonationConstraint) + copy(dAtA[i:], m.ImpersonationConstraint) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.ImpersonationConstraint))) + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil } func (m *Event) Marshal() (dAtA []byte, err error) { @@ -364,6 +100,20 @@ func (m *Event) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.AuthenticationMetadata != nil { + { + size, err := m.AuthenticationMetadata.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a + } i -= len(m.UserAgent) copy(dAtA[i:], m.UserAgent) i = encodeVarintGenerated(dAtA, i, uint64(len(m.UserAgent))) @@ -376,7 +126,7 @@ func (m *Event) MarshalToSizedBuffer(dAtA []byte) (int, error) { for k := range m.Annotations { keysForAnnotations = append(keysForAnnotations, string(k)) } - github_com_gogo_protobuf_sortkeys.Strings(keysForAnnotations) + sort.Strings(keysForAnnotations) for iNdEx := len(keysForAnnotations) - 1; iNdEx >= 0; iNdEx-- { v := m.Annotations[string(keysForAnnotations[iNdEx])] baseI := i @@ -906,6 +656,17 @@ func encodeVarintGenerated(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } +func (m *AuthenticationMetadata) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ImpersonationConstraint) + n += 1 + l + sovGenerated(uint64(l)) + return n +} + func (m *Event) Size() (n int) { if m == nil { return 0 @@ -964,6 +725,10 @@ func (m *Event) Size() (n int) { } l = len(m.UserAgent) n += 2 + l + sovGenerated(uint64(l)) + if m.AuthenticationMetadata != nil { + l = m.AuthenticationMetadata.Size() + n += 2 + l + sovGenerated(uint64(l)) + } return n } @@ -1135,6 +900,16 @@ func sovGenerated(x uint64) (n int) { func sozGenerated(x uint64) (n int) { return sovGenerated(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } +func (this *AuthenticationMetadata) String() string { + if this == nil { + return "nil" + } + s := strings.Join([]string{`&AuthenticationMetadata{`, + `ImpersonationConstraint:` + fmt.Sprintf("%v", this.ImpersonationConstraint) + `,`, + `}`, + }, "") + return s +} func (this *Event) String() string { if this == nil { return "nil" @@ -1143,7 +918,7 @@ func (this *Event) String() string { for k := range this.Annotations { keysForAnnotations = append(keysForAnnotations, k) } - github_com_gogo_protobuf_sortkeys.Strings(keysForAnnotations) + sort.Strings(keysForAnnotations) mapStringForAnnotations := "map[string]string{" for _, k := range keysForAnnotations { mapStringForAnnotations += fmt.Sprintf("%v: %v,", k, this.Annotations[k]) @@ -1166,6 +941,7 @@ func (this *Event) String() string { `StageTimestamp:` + strings.Replace(strings.Replace(fmt.Sprintf("%v", this.StageTimestamp), "MicroTime", "v11.MicroTime", 1), `&`, ``, 1) + `,`, `Annotations:` + mapStringForAnnotations + `,`, `UserAgent:` + fmt.Sprintf("%v", this.UserAgent) + `,`, + `AuthenticationMetadata:` + strings.Replace(this.AuthenticationMetadata.String(), "AuthenticationMetadata", "AuthenticationMetadata", 1) + `,`, `}`, }, "") return s @@ -1280,6 +1056,88 @@ func valueToStringGenerated(v interface{}) string { pv := reflect.Indirect(rv).Interface() return fmt.Sprintf("*%v", pv) } +func (m *AuthenticationMetadata) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AuthenticationMetadata: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AuthenticationMetadata: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ImpersonationConstraint", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ImpersonationConstraint = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenerated(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenerated + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *Event) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1939,6 +1797,42 @@ func (m *Event) Unmarshal(dAtA []byte) error { } m.UserAgent = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 17: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AuthenticationMetadata", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.AuthenticationMetadata == nil { + m.AuthenticationMetadata = &AuthenticationMetadata{} + } + if err := m.AuthenticationMetadata.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.proto b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.proto similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.proto rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.proto index 032c58691..db0afa5e4 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/generated.proto +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/generated.proto @@ -29,6 +29,13 @@ import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto"; // Package-wide variables from generator "generated". option go_package = "k8s.io/apiserver/pkg/apis/audit/v1"; +message AuthenticationMetadata { + // ImpersonationConstraint is the verb associated with the constrained impersonation mode that was used to authorize + // the ImpersonatedUser associated with this audit event. It is only set when constrained impersonation was used. + // +optional + optional string impersonationConstraint = 1; +} + // Event captures all the information that can be included in an API audit log. message Event { // AuditLevel at which event was generated @@ -54,6 +61,10 @@ message Event { // +optional optional .k8s.io.api.authentication.v1.UserInfo impersonatedUser = 7; + // AuthenticationMetadata contains details about how the request was authenticated. + // +optional + optional AuthenticationMetadata authenticationMetadata = 17; + // Source IPs, from where the request originated and intermediate proxies. // The source IPs are listed from (in order): // 1. X-Forwarded-For request header IPs @@ -124,6 +135,7 @@ message EventList { message GroupResources { // Group is the name of the API group that contains the resources. // The empty string represents the core API group. + // `*` matches all groups // +optional optional string group = 1; diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/types.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/types.go index ae122d6c4..8c6dc676a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/types.go @@ -90,6 +90,9 @@ type Event struct { // Impersonated user information. // +optional ImpersonatedUser *authnv1.UserInfo `json:"impersonatedUser,omitempty" protobuf:"bytes,7,opt,name=impersonatedUser"` + // AuthenticationMetadata contains details about how the request was authenticated. + // +optional + AuthenticationMetadata *AuthenticationMetadata `json:"authenticationMetadata,omitempty" protobuf:"bytes,17,opt,name=authenticationMetadata"` // Source IPs, from where the request originated and intermediate proxies. // The source IPs are listed from (in order): // 1. X-Forwarded-For request header IPs @@ -142,6 +145,13 @@ type Event struct { Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,15,rep,name=annotations"` } +type AuthenticationMetadata struct { + // ImpersonationConstraint is the verb associated with the constrained impersonation mode that was used to authorize + // the ImpersonatedUser associated with this audit event. It is only set when constrained impersonation was used. + // +optional + ImpersonationConstraint string `json:"impersonationConstraint,omitempty" protobuf:"bytes,1,opt,name=impersonationConstraint"` +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // EventList is a list of audit Events. @@ -268,6 +278,7 @@ type PolicyRule struct { type GroupResources struct { // Group is the name of the API group that contains the resources. // The empty string represents the core API group. + // `*` matches all groups // +optional Group string `json:"group,omitempty" protobuf:"bytes,1,opt,name=group"` // Resources is a list of resources this rule applies to. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.conversion.go similarity index 87% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.conversion.go index 53cbb0208..904d9c12b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.conversion.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.conversion.go @@ -39,6 +39,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*AuthenticationMetadata)(nil), (*audit.AuthenticationMetadata)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_AuthenticationMetadata_To_audit_AuthenticationMetadata(a.(*AuthenticationMetadata), b.(*audit.AuthenticationMetadata), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*audit.AuthenticationMetadata)(nil), (*AuthenticationMetadata)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_audit_AuthenticationMetadata_To_v1_AuthenticationMetadata(a.(*audit.AuthenticationMetadata), b.(*AuthenticationMetadata), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Event)(nil), (*audit.Event)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_Event_To_audit_Event(a.(*Event), b.(*audit.Event), scope) }); err != nil { @@ -112,6 +122,26 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1_AuthenticationMetadata_To_audit_AuthenticationMetadata(in *AuthenticationMetadata, out *audit.AuthenticationMetadata, s conversion.Scope) error { + out.ImpersonationConstraint = in.ImpersonationConstraint + return nil +} + +// Convert_v1_AuthenticationMetadata_To_audit_AuthenticationMetadata is an autogenerated conversion function. +func Convert_v1_AuthenticationMetadata_To_audit_AuthenticationMetadata(in *AuthenticationMetadata, out *audit.AuthenticationMetadata, s conversion.Scope) error { + return autoConvert_v1_AuthenticationMetadata_To_audit_AuthenticationMetadata(in, out, s) +} + +func autoConvert_audit_AuthenticationMetadata_To_v1_AuthenticationMetadata(in *audit.AuthenticationMetadata, out *AuthenticationMetadata, s conversion.Scope) error { + out.ImpersonationConstraint = in.ImpersonationConstraint + return nil +} + +// Convert_audit_AuthenticationMetadata_To_v1_AuthenticationMetadata is an autogenerated conversion function. +func Convert_audit_AuthenticationMetadata_To_v1_AuthenticationMetadata(in *audit.AuthenticationMetadata, out *AuthenticationMetadata, s conversion.Scope) error { + return autoConvert_audit_AuthenticationMetadata_To_v1_AuthenticationMetadata(in, out, s) +} + func autoConvert_v1_Event_To_audit_Event(in *Event, out *audit.Event, s conversion.Scope) error { out.Level = audit.Level(in.Level) out.AuditID = types.UID(in.AuditID) @@ -120,6 +150,7 @@ func autoConvert_v1_Event_To_audit_Event(in *Event, out *audit.Event, s conversi out.Verb = in.Verb out.User = in.User out.ImpersonatedUser = (*authenticationv1.UserInfo)(unsafe.Pointer(in.ImpersonatedUser)) + out.AuthenticationMetadata = (*audit.AuthenticationMetadata)(unsafe.Pointer(in.AuthenticationMetadata)) out.SourceIPs = *(*[]string)(unsafe.Pointer(&in.SourceIPs)) out.UserAgent = in.UserAgent out.ObjectRef = (*audit.ObjectReference)(unsafe.Pointer(in.ObjectRef)) @@ -145,6 +176,7 @@ func autoConvert_audit_Event_To_v1_Event(in *audit.Event, out *Event, s conversi out.Verb = in.Verb out.User = in.User out.ImpersonatedUser = (*authenticationv1.UserInfo)(unsafe.Pointer(in.ImpersonatedUser)) + out.AuthenticationMetadata = (*AuthenticationMetadata)(unsafe.Pointer(in.AuthenticationMetadata)) out.SourceIPs = *(*[]string)(unsafe.Pointer(&in.SourceIPs)) out.UserAgent = in.UserAgent out.ObjectRef = (*ObjectReference)(unsafe.Pointer(in.ObjectRef)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.deepcopy.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.deepcopy.go index 0b1b0052d..5b0c1fe1a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.deepcopy.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationMetadata) DeepCopyInto(out *AuthenticationMetadata) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationMetadata. +func (in *AuthenticationMetadata) DeepCopy() *AuthenticationMetadata { + if in == nil { + return nil + } + out := new(AuthenticationMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Event) DeepCopyInto(out *Event) { *out = *in @@ -37,6 +53,11 @@ func (in *Event) DeepCopyInto(out *Event) { *out = new(authenticationv1.UserInfo) (*in).DeepCopyInto(*out) } + if in.AuthenticationMetadata != nil { + in, out := &in.AuthenticationMetadata, &out.AuthenticationMetadata + *out = new(AuthenticationMetadata) + **out = **in + } if in.SourceIPs != nil { in, out := &in.SourceIPs, &out.SourceIPs *out = make([]string, len(*in)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.model_name.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.model_name.go new file mode 100644 index 000000000..b4f3d3823 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/v1/zz_generated.model_name.go @@ -0,0 +1,62 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1 + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in AuthenticationMetadata) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.AuthenticationMetadata" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Event) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.Event" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in EventList) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.EventList" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in GroupResources) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.GroupResources" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in ObjectReference) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.ObjectReference" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Policy) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.Policy" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in PolicyList) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.PolicyList" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in PolicyRule) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.apis.audit.v1.PolicyRule" +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/validation/validation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/validation/validation.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/validation/validation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/validation/validation.go index 0611c1ae5..3b0d077fd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/validation/validation.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/validation/validation.go @@ -98,7 +98,7 @@ func validateResources(groupResources []audit.GroupResources, fldPath *field.Pat var allErrs field.ErrorList for _, groupResource := range groupResources { // The empty string represents the core API group. - if len(groupResource.Group) != 0 { + if len(groupResource.Group) != 0 && groupResource.Group != "*" { // Group names must be lower case and be valid DNS subdomains. // reference: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md // an error is returned for group name like rbac.authorization.k8s.io/v1beta1 diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/validation/validation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/validation/validation_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/validation/validation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/validation/validation_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/zz_generated.deepcopy.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/zz_generated.deepcopy.go index 81d5add47..f8edfca49 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/audit/zz_generated.deepcopy.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/audit/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationMetadata) DeepCopyInto(out *AuthenticationMetadata) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationMetadata. +func (in *AuthenticationMetadata) DeepCopy() *AuthenticationMetadata { + if in == nil { + return nil + } + out := new(AuthenticationMetadata) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Event) DeepCopyInto(out *Event) { *out = *in @@ -37,6 +53,11 @@ func (in *Event) DeepCopyInto(out *Event) { *out = new(v1.UserInfo) (*in).DeepCopyInto(*out) } + if in.AuthenticationMetadata != nil { + in, out := &in.AuthenticationMetadata, &out.AuthenticationMetadata + *out = new(AuthenticationMetadata) + **out = **in + } if in.SourceIPs != nil { in, out := &in.SourceIPs, &out.SourceIPs *out = make([]string, len(*in)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/cel/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/cel/config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/cel/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/cel/config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/fuzzer/fuzzer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/fuzzer/fuzzer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/fuzzer/fuzzer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/fuzzer/fuzzer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/install/install.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/install/install.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/install/roundtrip_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/install/roundtrip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/install/roundtrip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/install/roundtrip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/generated.pb.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/generated.pb.go similarity index 80% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/generated.pb.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/generated.pb.go index 494413a14..15e75a392 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/generated.pb.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/generated.pb.go @@ -23,249 +23,24 @@ import ( fmt "fmt" io "io" + "sort" - proto "github.com/gogo/protobuf/proto" - github_com_gogo_protobuf_sortkeys "github.com/gogo/protobuf/sortkeys" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - math "math" math_bits "math/bits" reflect "reflect" strings "strings" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf +func (m *Pod) Reset() { *m = Pod{} } -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +func (m *PodCondition) Reset() { *m = PodCondition{} } -func (m *Pod) Reset() { *m = Pod{} } -func (*Pod) ProtoMessage() {} -func (*Pod) Descriptor() ([]byte, []int) { - return fileDescriptor_c0604dbfc428ecfb, []int{0} -} -func (m *Pod) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *Pod) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *Pod) XXX_Merge(src proto.Message) { - xxx_messageInfo_Pod.Merge(m, src) -} -func (m *Pod) XXX_Size() int { - return m.Size() -} -func (m *Pod) XXX_DiscardUnknown() { - xxx_messageInfo_Pod.DiscardUnknown(m) -} - -var xxx_messageInfo_Pod proto.InternalMessageInfo - -func (m *PodCondition) Reset() { *m = PodCondition{} } -func (*PodCondition) ProtoMessage() {} -func (*PodCondition) Descriptor() ([]byte, []int) { - return fileDescriptor_c0604dbfc428ecfb, []int{1} -} -func (m *PodCondition) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PodCondition) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PodCondition) XXX_Merge(src proto.Message) { - xxx_messageInfo_PodCondition.Merge(m, src) -} -func (m *PodCondition) XXX_Size() int { - return m.Size() -} -func (m *PodCondition) XXX_DiscardUnknown() { - xxx_messageInfo_PodCondition.DiscardUnknown(m) -} - -var xxx_messageInfo_PodCondition proto.InternalMessageInfo - -func (m *PodList) Reset() { *m = PodList{} } -func (*PodList) ProtoMessage() {} -func (*PodList) Descriptor() ([]byte, []int) { - return fileDescriptor_c0604dbfc428ecfb, []int{2} -} -func (m *PodList) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PodList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PodList) XXX_Merge(src proto.Message) { - xxx_messageInfo_PodList.Merge(m, src) -} -func (m *PodList) XXX_Size() int { - return m.Size() -} -func (m *PodList) XXX_DiscardUnknown() { - xxx_messageInfo_PodList.DiscardUnknown(m) -} +func (m *PodList) Reset() { *m = PodList{} } -var xxx_messageInfo_PodList proto.InternalMessageInfo +func (m *PodSpec) Reset() { *m = PodSpec{} } -func (m *PodSpec) Reset() { *m = PodSpec{} } -func (*PodSpec) ProtoMessage() {} -func (*PodSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_c0604dbfc428ecfb, []int{3} -} -func (m *PodSpec) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PodSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PodSpec) XXX_Merge(src proto.Message) { - xxx_messageInfo_PodSpec.Merge(m, src) -} -func (m *PodSpec) XXX_Size() int { - return m.Size() -} -func (m *PodSpec) XXX_DiscardUnknown() { - xxx_messageInfo_PodSpec.DiscardUnknown(m) -} - -var xxx_messageInfo_PodSpec proto.InternalMessageInfo - -func (m *PodStatus) Reset() { *m = PodStatus{} } -func (*PodStatus) ProtoMessage() {} -func (*PodStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_c0604dbfc428ecfb, []int{4} -} -func (m *PodStatus) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PodStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PodStatus) XXX_Merge(src proto.Message) { - xxx_messageInfo_PodStatus.Merge(m, src) -} -func (m *PodStatus) XXX_Size() int { - return m.Size() -} -func (m *PodStatus) XXX_DiscardUnknown() { - xxx_messageInfo_PodStatus.DiscardUnknown(m) -} - -var xxx_messageInfo_PodStatus proto.InternalMessageInfo - -func init() { - proto.RegisterType((*Pod)(nil), "k8s.io.apiserver.pkg.apis.example.v1.Pod") - proto.RegisterType((*PodCondition)(nil), "k8s.io.apiserver.pkg.apis.example.v1.PodCondition") - proto.RegisterType((*PodList)(nil), "k8s.io.apiserver.pkg.apis.example.v1.PodList") - proto.RegisterType((*PodSpec)(nil), "k8s.io.apiserver.pkg.apis.example.v1.PodSpec") - proto.RegisterMapType((map[string]string)(nil), "k8s.io.apiserver.pkg.apis.example.v1.PodSpec.NodeSelectorEntry") - proto.RegisterType((*PodStatus)(nil), "k8s.io.apiserver.pkg.apis.example.v1.PodStatus") -} - -func init() { - proto.RegisterFile("k8s.io/apiserver/pkg/apis/example/v1/generated.proto", fileDescriptor_c0604dbfc428ecfb) -} - -var fileDescriptor_c0604dbfc428ecfb = []byte{ - // 1039 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6e, 0xdb, 0xc6, - 0x13, 0x36, 0x2d, 0xcb, 0x92, 0xd6, 0x56, 0x62, 0x6f, 0x62, 0x80, 0x31, 0x10, 0xc9, 0xd1, 0x2f, - 0x30, 0x9c, 0x1f, 0x1a, 0xb2, 0x36, 0xd2, 0x22, 0x6d, 0x0f, 0x41, 0x68, 0x17, 0xb5, 0x02, 0x47, - 0x26, 0x56, 0x06, 0x02, 0x14, 0x3d, 0x74, 0x45, 0x4e, 0x24, 0x56, 0x22, 0x97, 0x20, 0x57, 0x6a, - 0x75, 0xeb, 0x23, 0xb4, 0x0f, 0xd0, 0xa7, 0xe8, 0xa1, 0x40, 0x9f, 0xc0, 0xc7, 0x1c, 0x73, 0x12, - 0x6a, 0xf5, 0x2d, 0x7c, 0x2a, 0x76, 0xf9, 0x47, 0xa4, 0xa5, 0xba, 0xf2, 0x6d, 0x77, 0xe6, 0xfb, - 0xbe, 0x19, 0xcd, 0x0e, 0x67, 0x84, 0x5e, 0xf4, 0x5f, 0x86, 0x9a, 0xc3, 0x74, 0xea, 0x3b, 0x21, - 0x04, 0x23, 0x08, 0x74, 0xbf, 0xdf, 0x95, 0x37, 0x1d, 0x7e, 0xa2, 0xae, 0x3f, 0x00, 0x7d, 0x74, - 0xa8, 0x77, 0xc1, 0x83, 0x80, 0x72, 0xb0, 0x35, 0x3f, 0x60, 0x9c, 0xe1, 0xa7, 0x11, 0x4b, 0x4b, - 0x59, 0x9a, 0xdf, 0xef, 0xca, 0x9b, 0x16, 0xb3, 0xb4, 0xd1, 0xe1, 0xee, 0xf3, 0xae, 0xc3, 0x7b, - 0xc3, 0x8e, 0x66, 0x31, 0x57, 0xef, 0xb2, 0x2e, 0xd3, 0x25, 0xb9, 0x33, 0x7c, 0x2f, 0x6f, 0xf2, - 0x22, 0x4f, 0x91, 0xe8, 0x6e, 0x26, 0x15, 0x97, 0x5a, 0x3d, 0xc7, 0x83, 0x60, 0x3c, 0xcb, 0xc6, - 0x05, 0x4e, 0x17, 0xa4, 0xb2, 0xab, 0xff, 0x1b, 0x2b, 0x18, 0x7a, 0xdc, 0x71, 0x61, 0x8e, 0xf0, - 0xf9, 0x7f, 0x11, 0x42, 0xab, 0x07, 0x2e, 0xbd, 0xc9, 0x6b, 0xfc, 0xba, 0x8a, 0x0a, 0x26, 0xb3, - 0xf1, 0xf7, 0xa8, 0x2c, 0x72, 0xb1, 0x29, 0xa7, 0xaa, 0xb2, 0xa7, 0x1c, 0x6c, 0x1c, 0x7d, 0xaa, - 0xcd, 0xca, 0x91, 0x4a, 0xce, 0x2a, 0x22, 0xd0, 0xda, 0xe8, 0x50, 0x3b, 0xef, 0xfc, 0x00, 0x16, - 0x7f, 0x0b, 0x9c, 0x1a, 0xf8, 0x72, 0x52, 0x5f, 0x99, 0x4e, 0xea, 0x68, 0x66, 0x23, 0xa9, 0x2a, - 0x3e, 0x47, 0x6b, 0xa1, 0x0f, 0x96, 0xba, 0x2a, 0xd5, 0x9f, 0x6b, 0xcb, 0x14, 0x5b, 0x33, 0x99, - 0xdd, 0xf6, 0xc1, 0x32, 0x36, 0x63, 0xe9, 0x35, 0x71, 0x23, 0x52, 0x08, 0xbf, 0x43, 0xeb, 0x21, - 0xa7, 0x7c, 0x18, 0xaa, 0x05, 0x29, 0xa9, 0x2f, 0x2f, 0x29, 0x69, 0xc6, 0xbd, 0x58, 0x74, 0x3d, - 0xba, 0x93, 0x58, 0xae, 0xf1, 0x7b, 0x01, 0x6d, 0x9a, 0xcc, 0x3e, 0x66, 0x9e, 0xed, 0x70, 0x87, - 0x79, 0xf8, 0x05, 0x5a, 0xe3, 0x63, 0x1f, 0x64, 0x61, 0x2a, 0xc6, 0x5e, 0x92, 0xcb, 0xc5, 0xd8, - 0x87, 0xeb, 0x49, 0x7d, 0x2b, 0x8b, 0x15, 0x36, 0x22, 0xd1, 0xf8, 0x8b, 0x34, 0xbf, 0x55, 0xc9, - 0x7b, 0x92, 0x0f, 0x77, 0x3d, 0xa9, 0xdf, 0x4f, 0x69, 0xf9, 0x0c, 0x70, 0x17, 0x55, 0x07, 0x34, - 0xe4, 0x66, 0xc0, 0x3a, 0x70, 0xe1, 0xb8, 0x10, 0xff, 0xc2, 0xff, 0x2f, 0xf7, 0x24, 0x82, 0x61, - 0xec, 0xc4, 0xd1, 0xaa, 0x67, 0x59, 0x21, 0x92, 0xd7, 0xc5, 0x23, 0x84, 0x85, 0xe1, 0x22, 0xa0, - 0x5e, 0x18, 0xe5, 0x2f, 0xa2, 0xad, 0xdd, 0x39, 0xda, 0x6e, 0x1c, 0x0d, 0x9f, 0xcd, 0xa9, 0x91, - 0x05, 0x11, 0xf0, 0x3e, 0x5a, 0x0f, 0x80, 0x86, 0xcc, 0x53, 0x8b, 0xb2, 0x36, 0xe9, 0x53, 0x10, - 0x69, 0x25, 0xb1, 0x17, 0x3f, 0x43, 0x25, 0x17, 0xc2, 0x90, 0x76, 0x41, 0x5d, 0x97, 0xc0, 0xfb, - 0x31, 0xb0, 0xf4, 0x36, 0x32, 0x93, 0xc4, 0xdf, 0xf8, 0x43, 0x41, 0x25, 0x93, 0xd9, 0x67, 0x4e, - 0xc8, 0xf1, 0x77, 0x73, 0xdd, 0xac, 0x2d, 0xf7, 0x63, 0x04, 0x5b, 0xf6, 0xf2, 0x56, 0x1c, 0xa7, - 0x9c, 0x58, 0x32, 0x9d, 0xdc, 0x42, 0x45, 0x87, 0x83, 0x2b, 0xde, 0xb5, 0x70, 0xb0, 0x71, 0xf4, - 0x6c, 0xe9, 0xbe, 0x33, 0xaa, 0xb1, 0x6a, 0xb1, 0x29, 0xf8, 0x24, 0x92, 0x69, 0xfc, 0x59, 0x92, - 0x99, 0x8b, 0xd6, 0xc6, 0x67, 0xa8, 0x1a, 0x40, 0xc8, 0x69, 0xc0, 0x4d, 0x36, 0x70, 0xac, 0xb1, - 0x7c, 0xf9, 0x8a, 0xb1, 0x9f, 0xbc, 0x26, 0xc9, 0x3a, 0xaf, 0x6f, 0x1a, 0x48, 0x9e, 0x8c, 0xbb, - 0xe8, 0x31, 0x87, 0xc0, 0x75, 0x3c, 0x2a, 0x2a, 0xff, 0x4d, 0x40, 0x2d, 0x30, 0x21, 0x70, 0x98, - 0xdd, 0x06, 0x8b, 0x79, 0x76, 0x28, 0x5f, 0xba, 0x60, 0x3c, 0x99, 0x4e, 0xea, 0x8f, 0x2f, 0x6e, - 0x03, 0x92, 0xdb, 0x75, 0xf0, 0x39, 0xda, 0xa1, 0x16, 0x77, 0x46, 0x70, 0x02, 0xd4, 0x1e, 0x38, - 0x1e, 0x24, 0x01, 0x8a, 0x32, 0xc0, 0xa3, 0xe9, 0xa4, 0xbe, 0xf3, 0x7a, 0x11, 0x80, 0x2c, 0xe6, - 0xe1, 0x31, 0xda, 0xf4, 0x98, 0x0d, 0x6d, 0x18, 0x80, 0xc5, 0x59, 0xa0, 0x96, 0x64, 0xa9, 0x5f, - 0xdd, 0x69, 0x6a, 0x68, 0xad, 0x8c, 0xc2, 0xd7, 0x1e, 0x0f, 0xc6, 0xc6, 0xc3, 0xb8, 0x8e, 0x9b, - 0x59, 0x17, 0xc9, 0x85, 0xc2, 0x6f, 0x10, 0x16, 0xda, 0x8e, 0x05, 0xaf, 0x2d, 0x8b, 0x0d, 0x3d, - 0xde, 0xa2, 0x2e, 0xa8, 0x65, 0xf9, 0x0e, 0x69, 0x9f, 0xb7, 0xe7, 0x10, 0x64, 0x01, 0x0b, 0x9f, - 0xa2, 0x7b, 0x79, 0xab, 0x5a, 0xc9, 0xcd, 0x10, 0xf5, 0x04, 0xfc, 0x00, 0x2c, 0x31, 0x90, 0xf3, - 0x8a, 0xe4, 0x06, 0x0f, 0x7f, 0x82, 0xca, 0x22, 0x4b, 0x99, 0x0b, 0x92, 0x1a, 0x69, 0x8b, 0xb6, - 0x62, 0x3b, 0x49, 0x11, 0xf8, 0x33, 0xb4, 0xd1, 0x63, 0x21, 0x6f, 0x01, 0xff, 0x91, 0x05, 0x7d, - 0x75, 0x63, 0x4f, 0x39, 0x28, 0x1b, 0x0f, 0x62, 0xc2, 0xc6, 0xe9, 0xcc, 0x45, 0xb2, 0x38, 0xf1, - 0xb9, 0x89, 0xab, 0xd9, 0x3c, 0x51, 0x37, 0x25, 0x25, 0xfd, 0xdc, 0x4e, 0x23, 0x33, 0x49, 0xfc, - 0x09, 0xb4, 0x69, 0x1e, 0xab, 0xd5, 0x79, 0x68, 0xd3, 0x3c, 0x26, 0x89, 0x5f, 0xa4, 0x2e, 0x8e, - 0x9e, 0x48, 0x7d, 0x2b, 0x9f, 0xfa, 0x69, 0x6c, 0x27, 0x29, 0x02, 0xeb, 0xa8, 0x12, 0x0e, 0x3b, - 0x36, 0x73, 0xa9, 0xe3, 0xa9, 0xdb, 0x12, 0xbe, 0x1d, 0xc3, 0x2b, 0xed, 0xc4, 0x41, 0x66, 0x18, - 0xfc, 0x15, 0xaa, 0x8a, 0xe5, 0x66, 0x0f, 0x07, 0x10, 0xc8, 0x18, 0x0f, 0x24, 0x29, 0x1d, 0x80, - 0xed, 0xc4, 0x29, 0x6b, 0x94, 0xc7, 0xee, 0xbe, 0x42, 0xdb, 0x73, 0x5d, 0x82, 0xb7, 0x50, 0xa1, - 0x0f, 0xe3, 0x68, 0xdc, 0x13, 0x71, 0xc4, 0x0f, 0x51, 0x71, 0x44, 0x07, 0x43, 0x88, 0x46, 0x39, - 0x89, 0x2e, 0x5f, 0xae, 0xbe, 0x54, 0x1a, 0xbf, 0x15, 0x50, 0x25, 0x5d, 0x29, 0x58, 0x47, 0x45, - 0xbf, 0x47, 0xc3, 0x64, 0x55, 0x3c, 0x4a, 0xbe, 0x77, 0x53, 0x18, 0xaf, 0x27, 0xf5, 0xb2, 0xc9, - 0x6c, 0x79, 0x26, 0x11, 0x0e, 0xbf, 0x47, 0xc8, 0x4a, 0x96, 0x40, 0x32, 0x50, 0x8e, 0x96, 0xee, - 0xf2, 0x74, 0x7f, 0xcc, 0x76, 0x6f, 0x6a, 0x0a, 0x49, 0x46, 0x39, 0x3b, 0x48, 0x0b, 0xb7, 0x0f, - 0xd2, 0xcc, 0x6c, 0x5e, 0xbb, 0x75, 0x36, 0xef, 0xa3, 0xf5, 0xe8, 0x85, 0x6f, 0xce, 0xf0, 0xa8, - 0x01, 0x48, 0xec, 0xc5, 0xff, 0x43, 0x45, 0x9f, 0xd9, 0x4d, 0x33, 0x9e, 0xe0, 0xe9, 0x0c, 0x34, - 0x85, 0x91, 0x44, 0x3e, 0xfc, 0x0e, 0x55, 0xe4, 0xe0, 0x92, 0xfb, 0xa7, 0x74, 0xe7, 0xfd, 0x53, - 0x95, 0xdd, 0x91, 0x08, 0x90, 0x99, 0x96, 0xf1, 0xe6, 0xf2, 0xaa, 0xb6, 0xf2, 0xe1, 0xaa, 0xb6, - 0xf2, 0xf1, 0xaa, 0xb6, 0xf2, 0xf3, 0xb4, 0xa6, 0x5c, 0x4e, 0x6b, 0xca, 0x87, 0x69, 0x4d, 0xf9, - 0x38, 0xad, 0x29, 0x7f, 0x4d, 0x6b, 0xca, 0x2f, 0x7f, 0xd7, 0x56, 0xbe, 0x7d, 0xba, 0xcc, 0x1f, - 0xc6, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xfb, 0x77, 0xce, 0x8b, 0x57, 0x0a, 0x00, 0x00, -} +func (m *PodStatus) Reset() { *m = PodStatus{} } func (m *Pod) Marshal() (dAtA []byte, err error) { size := m.Size() @@ -515,7 +290,7 @@ func (m *PodSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { for k := range m.NodeSelector { keysForNodeSelector = append(keysForNodeSelector, string(k)) } - github_com_gogo_protobuf_sortkeys.Strings(keysForNodeSelector) + sort.Strings(keysForNodeSelector) for iNdEx := len(keysForNodeSelector) - 1; iNdEx >= 0; iNdEx-- { v := m.NodeSelector[string(keysForNodeSelector[iNdEx])] baseI := i @@ -816,7 +591,7 @@ func (this *PodSpec) String() string { for k := range this.NodeSelector { keysForNodeSelector = append(keysForNodeSelector, k) } - github_com_gogo_protobuf_sortkeys.Strings(keysForNodeSelector) + sort.Strings(keysForNodeSelector) mapStringForNodeSelector := "map[string]string{" for _, k := range keysForNodeSelector { mapStringForNodeSelector += fmt.Sprintf("%v: %v,", k, this.NodeSelector[k]) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/generated.proto b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/generated.proto similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/generated.proto rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/generated.proto diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/install/install.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/install/install.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/install/install.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/install/install.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/install/roundtrip_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/install/roundtrip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/install/roundtrip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/install/roundtrip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/generated.pb.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/generated.pb.go similarity index 69% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/generated.pb.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/generated.pb.go index 69e0826a7..1c3fa6b21 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/generated.pb.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/generated.pb.go @@ -24,147 +24,16 @@ import ( io "io" - proto "github.com/gogo/protobuf/proto" - - math "math" math_bits "math/bits" reflect "reflect" strings "strings" ) -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package - -func (m *ReplicaSet) Reset() { *m = ReplicaSet{} } -func (*ReplicaSet) ProtoMessage() {} -func (*ReplicaSet) Descriptor() ([]byte, []int) { - return fileDescriptor_c0d8f6d73eb5bf83, []int{0} -} -func (m *ReplicaSet) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *ReplicaSet) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *ReplicaSet) XXX_Merge(src proto.Message) { - xxx_messageInfo_ReplicaSet.Merge(m, src) -} -func (m *ReplicaSet) XXX_Size() int { - return m.Size() -} -func (m *ReplicaSet) XXX_DiscardUnknown() { - xxx_messageInfo_ReplicaSet.DiscardUnknown(m) -} +func (m *ReplicaSet) Reset() { *m = ReplicaSet{} } -var xxx_messageInfo_ReplicaSet proto.InternalMessageInfo +func (m *ReplicaSetSpec) Reset() { *m = ReplicaSetSpec{} } -func (m *ReplicaSetSpec) Reset() { *m = ReplicaSetSpec{} } -func (*ReplicaSetSpec) ProtoMessage() {} -func (*ReplicaSetSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_c0d8f6d73eb5bf83, []int{1} -} -func (m *ReplicaSetSpec) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *ReplicaSetSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *ReplicaSetSpec) XXX_Merge(src proto.Message) { - xxx_messageInfo_ReplicaSetSpec.Merge(m, src) -} -func (m *ReplicaSetSpec) XXX_Size() int { - return m.Size() -} -func (m *ReplicaSetSpec) XXX_DiscardUnknown() { - xxx_messageInfo_ReplicaSetSpec.DiscardUnknown(m) -} - -var xxx_messageInfo_ReplicaSetSpec proto.InternalMessageInfo - -func (m *ReplicaSetStatus) Reset() { *m = ReplicaSetStatus{} } -func (*ReplicaSetStatus) ProtoMessage() {} -func (*ReplicaSetStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_c0d8f6d73eb5bf83, []int{2} -} -func (m *ReplicaSetStatus) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *ReplicaSetStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *ReplicaSetStatus) XXX_Merge(src proto.Message) { - xxx_messageInfo_ReplicaSetStatus.Merge(m, src) -} -func (m *ReplicaSetStatus) XXX_Size() int { - return m.Size() -} -func (m *ReplicaSetStatus) XXX_DiscardUnknown() { - xxx_messageInfo_ReplicaSetStatus.DiscardUnknown(m) -} - -var xxx_messageInfo_ReplicaSetStatus proto.InternalMessageInfo - -func init() { - proto.RegisterType((*ReplicaSet)(nil), "k8s.io.apiserver.pkg.apis.example2.v1.ReplicaSet") - proto.RegisterType((*ReplicaSetSpec)(nil), "k8s.io.apiserver.pkg.apis.example2.v1.ReplicaSetSpec") - proto.RegisterType((*ReplicaSetStatus)(nil), "k8s.io.apiserver.pkg.apis.example2.v1.ReplicaSetStatus") -} - -func init() { - proto.RegisterFile("k8s.io/apiserver/pkg/apis/example2/v1/generated.proto", fileDescriptor_c0d8f6d73eb5bf83) -} - -var fileDescriptor_c0d8f6d73eb5bf83 = []byte{ - // 388 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0xc1, 0x6a, 0xe2, 0x40, - 0x18, 0xc7, 0x13, 0xd7, 0x15, 0x99, 0x15, 0x91, 0x9c, 0xc4, 0xc3, 0xb8, 0x08, 0x82, 0x87, 0xdd, - 0x99, 0x55, 0xd6, 0xdd, 0xd2, 0x53, 0xc9, 0xb5, 0x94, 0x42, 0x3c, 0x14, 0x7a, 0x69, 0xc7, 0xf8, - 0x35, 0xa6, 0x1a, 0x33, 0x64, 0x26, 0xa1, 0xbd, 0xf5, 0x11, 0xfa, 0x18, 0x7d, 0x14, 0x8f, 0x1e, - 0x3d, 0x49, 0x4d, 0x5f, 0xa4, 0x38, 0x49, 0x13, 0xaa, 0x96, 0xda, 0x5b, 0xfe, 0x93, 0xf9, 0xfd, - 0xbe, 0x3f, 0x1f, 0x83, 0xfa, 0x93, 0x23, 0x41, 0x5c, 0x9f, 0x32, 0xee, 0x0a, 0x08, 0x22, 0x08, - 0x28, 0x9f, 0x38, 0x2a, 0x51, 0xb8, 0x63, 0x1e, 0x9f, 0x42, 0x8f, 0x46, 0x5d, 0xea, 0xc0, 0x0c, - 0x02, 0x26, 0x61, 0x44, 0x78, 0xe0, 0x4b, 0xdf, 0x68, 0x27, 0x18, 0xc9, 0x30, 0xc2, 0x27, 0x8e, - 0x4a, 0xe4, 0x0d, 0x23, 0x51, 0xb7, 0xf1, 0xdb, 0x71, 0xe5, 0x38, 0x1c, 0x12, 0xdb, 0xf7, 0xa8, - 0xe3, 0x3b, 0x3e, 0x55, 0xf4, 0x30, 0xbc, 0x51, 0x49, 0x05, 0xf5, 0x95, 0x58, 0x1b, 0x7f, 0xf3, - 0x32, 0x1e, 0xb3, 0xc7, 0xee, 0x0c, 0x82, 0xfb, 0xbc, 0x8f, 0x07, 0x92, 0xed, 0xe9, 0xd2, 0xa0, - 0x1f, 0x51, 0x41, 0x38, 0x93, 0xae, 0x07, 0x3b, 0xc0, 0xbf, 0xcf, 0x00, 0x61, 0x8f, 0xc1, 0x63, - 0xdb, 0x5c, 0xeb, 0xa9, 0x80, 0x90, 0x05, 0x7c, 0xea, 0xda, 0x6c, 0x00, 0xd2, 0xb8, 0x46, 0xe5, - 0x4d, 0xa5, 0x11, 0x93, 0xac, 0xae, 0xff, 0xd4, 0x3b, 0x3f, 0x7a, 0x7f, 0x48, 0xbe, 0x96, 0xcc, - 0x9c, 0x6f, 0x66, 0x73, 0x9b, 0x44, 0x5d, 0x72, 0x3e, 0xbc, 0x05, 0x5b, 0x9e, 0x81, 0x64, 0xa6, - 0x31, 0x5f, 0x35, 0xb5, 0x78, 0xd5, 0x44, 0xf9, 0x99, 0x95, 0x59, 0x8d, 0x0b, 0x54, 0x14, 0x1c, - 0xec, 0x7a, 0x41, 0xd9, 0xfb, 0xe4, 0xa0, 0xa5, 0x93, 0xbc, 0xe2, 0x80, 0x83, 0x6d, 0x56, 0xd2, - 0x11, 0xc5, 0x4d, 0xb2, 0x94, 0xd0, 0xb8, 0x42, 0x25, 0x21, 0x99, 0x0c, 0x45, 0xfd, 0x9b, 0x52, - 0xff, 0xff, 0xba, 0x5a, 0xe1, 0x66, 0x35, 0x95, 0x97, 0x92, 0x6c, 0xa5, 0xda, 0xd6, 0x31, 0xaa, - 0xbe, 0xaf, 0x61, 0x74, 0x50, 0x39, 0x48, 0x4e, 0x84, 0xda, 0xd6, 0x77, 0xb3, 0x12, 0xaf, 0x9a, - 0xe5, 0xf4, 0x96, 0xb0, 0xb2, 0xbf, 0xad, 0x13, 0x54, 0xdb, 0x9e, 0x63, 0xfc, 0xda, 0xa1, 0x6b, - 0xe9, 0xe4, 0x3d, 0x06, 0xf3, 0x74, 0xbe, 0xc6, 0xda, 0x62, 0x8d, 0xb5, 0xe5, 0x1a, 0x6b, 0x0f, - 0x31, 0xd6, 0xe7, 0x31, 0xd6, 0x17, 0x31, 0xd6, 0x97, 0x31, 0xd6, 0x9f, 0x63, 0xac, 0x3f, 0xbe, - 0x60, 0xed, 0xb2, 0x7d, 0xd0, 0xd3, 0x7f, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xf4, 0xd7, 0x7c, 0xa3, - 0x22, 0x03, 0x00, 0x00, -} +func (m *ReplicaSetStatus) Reset() { *m = ReplicaSetStatus{} } func (m *ReplicaSet) Marshal() (dAtA []byte, err error) { size := m.Size() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/generated.proto b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/generated.proto similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/generated.proto rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/generated.proto diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/register.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/register.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/register.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.defaults.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/v1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/v1/zz_generated.defaults.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/example2/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/example2/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/flowcontrol/bootstrap/default.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/flowcontrol/bootstrap/default.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/flowcontrol/bootstrap/default.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/flowcontrol/bootstrap/default.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/apis/flowcontrol/bootstrap/default_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/apis/flowcontrol/bootstrap/default_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/apis/flowcontrol/bootstrap/default_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/apis/flowcontrol/bootstrap/default_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context.go index 5b93d594b..61d6ff5ef 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/context.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context.go @@ -132,7 +132,7 @@ func (ac *AuditContext) ProcessEventStage(ctx context.Context, stage auditintern return processed } -func (ac *AuditContext) LogImpersonatedUser(user user.Info) { +func (ac *AuditContext) LogImpersonatedUser(user user.Info, constraint string) { ac.visitEvent(func(ev *auditinternal.Event) { if ev == nil || ev.Level.Less(auditinternal.LevelMetadata) { return @@ -146,6 +146,12 @@ func (ac *AuditContext) LogImpersonatedUser(user user.Info) { for k, v := range user.GetExtra() { ev.ImpersonatedUser.Extra[k] = authnv1.ExtraValue(v) } + if len(constraint) > 0 { + if ev.AuthenticationMetadata == nil { + ev.AuthenticationMetadata = &auditinternal.AuthenticationMetadata{} + } + ev.AuthenticationMetadata.ImpersonationConstraint = constraint + } }) } @@ -178,6 +184,25 @@ func (ac *AuditContext) LogRequestPatch(patch []byte) { }) } +// GetEventUser returns a copy of the User associated with the audit Event. +func (ac *AuditContext) GetEventUser() authnv1.UserInfo { + var val *authnv1.UserInfo + ac.visitEvent(func(ev *auditinternal.Event) { + val = ev.User.DeepCopy() + }) + return *val +} + +// GetEventImpersonatedUser returns a copy of the ImpersonatedUser associated with the audit Event, +// or returns nil when there is no ImpersonatedUser. +func (ac *AuditContext) GetEventImpersonatedUser() *authnv1.UserInfo { + var val *authnv1.UserInfo + ac.visitEvent(func(ev *auditinternal.Event) { + val = ev.ImpersonatedUser.DeepCopy() + }) + return val +} + func (ac *AuditContext) GetEventAnnotation(key string) (string, bool) { var val string var ok bool diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context_test.go new file mode 100644 index 000000000..83753ade1 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/context_test.go @@ -0,0 +1,339 @@ +/* +Copyright 2021 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 audit + +import ( + "context" + "fmt" + "sync" + "testing" + + authnv1 "k8s.io/api/authentication/v1" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnabled(t *testing.T) { + tests := []struct { + name string + ctx *AuditContext + expectEnabled bool + }{{ + name: "nil context", + expectEnabled: false, + }, { + name: "empty context", + ctx: &AuditContext{}, + expectEnabled: true, // An AuditContext should be considered enabled before the level is set + }, { + name: "level None", + ctx: func() *AuditContext { + ctx := &AuditContext{} + if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelNone}, nil); err != nil { + t.Fatal(err) + } + return ctx + }(), + expectEnabled: false, + }, { + name: "level Metadata", + ctx: func() *AuditContext { + ctx := &AuditContext{} + if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil { + t.Fatal(err) + } + return ctx + }(), + expectEnabled: true, + }, { + name: "level RequestResponse", + ctx: func() *AuditContext { + ctx := &AuditContext{} + if err := ctx.Init(RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, nil); err != nil { + t.Fatal(err) + } + return ctx + }(), + expectEnabled: true, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectEnabled, test.ctx.Enabled()) + }) + } +} + +func TestAddAuditAnnotation(t *testing.T) { + const ( + annotationKeyTemplate = "test-annotation-%d" + annotationValue = "test-annotation-value" + annotationExtraValue = "test-existing-annotation" + numAnnotations = 10 + ) + + expectAnnotations := func(t *testing.T, annotations map[string]string) { + assert.Len(t, annotations, numAnnotations) + } + + ctxWithAnnotation := withAuditContextAndLevel(context.Background(), t, auditinternal.LevelMetadata) + AddAuditAnnotation(ctxWithAnnotation, fmt.Sprintf(annotationKeyTemplate, 0), annotationExtraValue) + + tests := []struct { + description string + ctx context.Context + validator func(t *testing.T, ctx context.Context) + }{{ + description: "no audit", + ctx: context.Background(), + validator: func(_ *testing.T, _ context.Context) {}, + }, { + description: "context initialized, policy not evaluated", + // Audit context is initialized, but the policy has not yet been evaluated (no level). + // Annotations should be retained. + ctx: WithAuditContext(context.Background()), + validator: func(t *testing.T, ctx context.Context) { + ev := AuditContextFrom(ctx).event + expectAnnotations(t, ev.Annotations) + }, + }, { + description: "with metadata level", + ctx: withAuditContextAndLevel(context.Background(), t, auditinternal.LevelMetadata), + validator: func(t *testing.T, ctx context.Context) { + ev := AuditContextFrom(ctx).event + expectAnnotations(t, ev.Annotations) + }, + }, { + description: "with none level", + ctx: withAuditContextAndLevel(context.Background(), t, auditinternal.LevelNone), + validator: func(t *testing.T, ctx context.Context) { + ev := AuditContextFrom(ctx).event + assert.Empty(t, ev.Annotations) + }, + }, { + description: "with overlapping annotations", + ctx: ctxWithAnnotation, + validator: func(t *testing.T, ctx context.Context) { + ev := AuditContextFrom(ctx).event + expectAnnotations(t, ev.Annotations) + // Verify that the pre-existing annotation is not overwritten. + assert.Equal(t, annotationExtraValue, ev.Annotations[fmt.Sprintf(annotationKeyTemplate, 0)]) + }, + }} + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + var wg sync.WaitGroup + wg.Add(numAnnotations) + for i := 0; i < numAnnotations; i++ { + go func(i int) { + AddAuditAnnotation(test.ctx, fmt.Sprintf(annotationKeyTemplate, i), annotationValue) + wg.Done() + }(i) + } + wg.Wait() + + test.validator(t, test.ctx) + }) + } +} + +func TestAuditAnnotationsWithAuditLoggingSetup(t *testing.T) { + // No audit context data in the request context + ctx := context.Background() + AddAuditAnnotation(ctx, "nil", "0") + + // initialize audit context, policy not evaluated yet + ctx = WithAuditContext(ctx) + AddAuditAnnotation(ctx, "before-evaluation", "1") + + // policy evaluated, audit logging enabled + if err := AuditContextFrom(ctx).Init(RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil { + t.Fatal(err) + } + AddAuditAnnotation(ctx, "after-evaluation", "2") + + expected := map[string]string{ + "before-evaluation": "1", + "after-evaluation": "2", + } + actual := AuditContextFrom(ctx).event.Annotations + assert.Equal(t, expected, actual) +} + +func TestGetEventUser(t *testing.T) { + tests := []struct { + name string + auditEventUser authnv1.UserInfo + wantUser authnv1.UserInfo + }{ + { + name: "fields with zero values are returned as fields with zero values", + auditEventUser: authnv1.UserInfo{}, + wantUser: authnv1.UserInfo{}, + }, + { + name: "fields with non-zero values are returned as copies", + auditEventUser: authnv1.UserInfo{ + Username: "test-user", + UID: "test-uid", + Groups: []string{"test-group1", "test-group2"}, + Extra: map[string]authnv1.ExtraValue{ + "test-extra1": {"test-extra1-val1", "test-extra1-val2"}, + "test-extra2": {"test-extra2-val1", "test-extra2-val2"}, + }, + }, + wantUser: authnv1.UserInfo{ + Username: "test-user", + UID: "test-uid", + Groups: []string{"test-group1", "test-group2"}, + Extra: map[string]authnv1.ExtraValue{ + "test-extra1": {"test-extra1-val1", "test-extra1-val2"}, + "test-extra2": {"test-extra2-val1", "test-extra2-val2"}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ac := AuditContext{event: auditinternal.Event{User: test.auditEventUser}} + got := ac.GetEventUser() + require.Equal(t, test.wantUser, got) + }) + } + + t.Run("mutating the returned groups does not change the audit event's User's groups", func(t *testing.T) { + ac := AuditContext{ + event: auditinternal.Event{ + User: authnv1.UserInfo{ + Groups: []string{"test-group1", "test-group2"}, + }, + }, + } + got := ac.GetEventUser() + require.Equal(t, []string{"test-group1", "test-group2"}, got.Groups) + got.Groups[0] = "mutated group" + require.Equal(t, []string{"mutated group", "test-group2"}, got.Groups) + // The event's groups are not changed. + require.Equal(t, []string{"test-group1", "test-group2"}, ac.event.User.Groups) + }) + + t.Run("mutating the returned extras does not change the audit event's User's extras", func(t *testing.T) { + ac := AuditContext{ + event: auditinternal.Event{ + User: authnv1.UserInfo{ + Extra: map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, + }, + }, + } + got := ac.GetEventUser() + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, got.Extra) + got.Extra["test-extra"] = authnv1.ExtraValue{"mutated value"} + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"mutated value"}}, got.Extra) + // The event's extras are not changed. + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, ac.event.User.Extra) + }) +} + +func TestGetEventImpersonatedUser(t *testing.T) { + tests := []struct { + name string + auditEventImpersonatedUser *authnv1.UserInfo + wantUser *authnv1.UserInfo + }{ + { + name: "nil ImpersonatedUser returns nil", + auditEventImpersonatedUser: nil, + wantUser: nil, + }, + { + name: "fields with zero values are returned as fields with zero values", + auditEventImpersonatedUser: &authnv1.UserInfo{}, + wantUser: &authnv1.UserInfo{}, + }, + { + name: "fields with non-zero values are returned as copies", + auditEventImpersonatedUser: &authnv1.UserInfo{ + Username: "test-user", + UID: "test-uid", + Groups: []string{"test-group1", "test-group2"}, + Extra: map[string]authnv1.ExtraValue{ + "test-extra1": {"test-extra1-val1", "test-extra1-val2"}, + "test-extra2": {"test-extra2-val1", "test-extra2-val2"}, + }, + }, + wantUser: &authnv1.UserInfo{ + Username: "test-user", + UID: "test-uid", + Groups: []string{"test-group1", "test-group2"}, + Extra: map[string]authnv1.ExtraValue{ + "test-extra1": {"test-extra1-val1", "test-extra1-val2"}, + "test-extra2": {"test-extra2-val1", "test-extra2-val2"}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ac := AuditContext{event: auditinternal.Event{ImpersonatedUser: test.auditEventImpersonatedUser}} + got := ac.GetEventImpersonatedUser() + require.Equal(t, test.wantUser, got) + }) + } + + t.Run("mutating the returned groups does not change the audit event's ImpersonatedUser's groups", func(t *testing.T) { + ac := AuditContext{ + event: auditinternal.Event{ + ImpersonatedUser: &authnv1.UserInfo{ + Groups: []string{"test-group1", "test-group2"}, + }, + }, + } + got := ac.GetEventImpersonatedUser() + require.Equal(t, []string{"test-group1", "test-group2"}, got.Groups) + got.Groups[0] = "mutated group" + require.Equal(t, []string{"mutated group", "test-group2"}, got.Groups) + // The event's groups are not changed. + require.Equal(t, []string{"test-group1", "test-group2"}, ac.event.ImpersonatedUser.Groups) + }) + + t.Run("mutating the returned extras does not change the audit event's ImpersonatedUser's extras", func(t *testing.T) { + ac := AuditContext{ + event: auditinternal.Event{ + ImpersonatedUser: &authnv1.UserInfo{ + Extra: map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, + }, + }, + } + got := ac.GetEventImpersonatedUser() + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, got.Extra) + got.Extra["test-extra"] = authnv1.ExtraValue{"mutated value"} + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"mutated value"}}, got.Extra) + // The event's extras are not changed. + require.Equal(t, map[string]authnv1.ExtraValue{"test-extra": {"test-extra-val"}}, ac.event.ImpersonatedUser.Extra) + }) +} + +func withAuditContextAndLevel(ctx context.Context, t *testing.T, l auditinternal.Level) context.Context { + ctx = WithAuditContext(ctx) + if err := AuditContextFrom(ctx).Init(RequestAuditConfig{Level: l}, nil); err != nil { + t.Fatal(err) + } + return ctx +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/evaluator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/evaluator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/evaluator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/evaluator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/format.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/format.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/format.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/format.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker.go index cd6ec92bc..9c21e2348 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker.go @@ -191,7 +191,7 @@ func ruleMatchesResource(r *audit.PolicyRule, attrs authorizer.Attributes) bool name := attrs.GetName() for _, gr := range r.Resources { - if gr.Group == apiGroup { + if gr.Group == apiGroup || gr.Group == "*" { if len(gr.Resources) == 0 { return true } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker_test.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker_test.go index 1e889c3c6..da747a0be 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/checker_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/checker_test.go @@ -84,6 +84,17 @@ var ( ResourceRequest: true, Path: "/api/v1/namespaces/default/pods/busybox", }, + "ClusterRoleUpdate": &authorizer.AttributesRecord{ + User: tim, + Verb: "update", + APIGroup: "rbac.authorization.k8s.io", + APIVersion: "v1beta1", + Resource: "clusterroles", + Subresource: "status", + Name: "somerole", + ResourceRequest: true, + Path: "/apis/rbac.authorization.k8s.io/v1beta1/clusterroles/somerole", + }, } rules = map[string]audit.PolicyRule{ @@ -140,6 +151,14 @@ var ( }}, Namespaces: []string{""}, }, + "getGroupWildCardMatchingWithSubresourceWildcardMatching": { + Level: audit.LevelRequestResponse, + Verbs: []string{"update"}, + Resources: []audit.GroupResources{{ + Group: "*", + Resources: []string{"*/status"}, + }}, + }, "getLogs": { Level: audit.LevelRequestResponse, Verbs: []string{"get"}, @@ -242,6 +261,7 @@ func testAuditLevel(t *testing.T, stages []audit.Stage) { test(t, "Unauthorized", audit.LevelMetadata, stages, stages, "tims", "default") test(t, "Unauthorized", audit.LevelNone, stages, stages, "humans") test(t, "Unauthorized", audit.LevelMetadata, stages, stages, "humans", "default") + test(t, "ClusterRoleUpdate", audit.LevelRequestResponse, stages, stages, "getGroupWildCardMatchingWithSubresourceWildcardMatching") } func TestChecker(t *testing.T) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/reader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/reader.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/reader.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/reader.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/reader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/reader_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/reader_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/reader_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/util_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/util_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/policy/util_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/policy/util_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request.go index d5f9c730f..4b076b090 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request.go @@ -82,12 +82,12 @@ func LogRequestMetadata(ctx context.Context, req *http.Request, requestReceivedT } // LogImpersonatedUser fills in the impersonated user attributes into an audit event. -func LogImpersonatedUser(ctx context.Context, user user.Info) { +func LogImpersonatedUser(ctx context.Context, user user.Info, constraint string) { ac := AuditContextFrom(ctx) if !ac.Enabled() { return } - ac.LogImpersonatedUser(user) + ac.LogImpersonatedUser(user, constraint) } // LogRequestObject fills in the request object into an audit event. The passed runtime.Object diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request_log_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request_log_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request_log_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request_log_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/request_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/request_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/scheme.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/scheme.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/scheme.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/scheme.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/union.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/union.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/union.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/union.go index 0766a9207..39dd74f74 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/union.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/union.go @@ -48,7 +48,6 @@ func (u union) ProcessEvents(events ...*auditinternal.Event) bool { func (u union) Run(stopCh <-chan struct{}) error { var funcs []func() error for _, backend := range u.backends { - backend := backend funcs = append(funcs, func() error { return backend.Run(stopCh) }) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/audit/union_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/audit/union_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/audit/union_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/audit/union_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audagnostic.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audagnostic.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audagnostic.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audagnostic.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audagnostic_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audagnostic_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audagnostic_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audagnostic_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audiences.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audiences.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audiences.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audiences.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audiences_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audiences_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/audiences_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/audiences_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticator/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticator/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/delegating.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/delegating.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/delegating.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/delegating.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/loopback.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/loopback.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/loopback.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/loopback.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/requestheader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/requestheader.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/authenticatorfactory/requestheader.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/authenticatorfactory/requestheader.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/compile.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/compile.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/compile.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/compile.go index 8c74e7ad4..6f25353cc 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/compile.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/compile.go @@ -42,7 +42,7 @@ type compiler struct { // NewDefaultCompiler returns a new Compiler following the default compatibility version. // Note: the compiler construction depends on feature gates and the compatibility version to be initialized. func NewDefaultCompiler() Compiler { - return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) } // NewCompiler returns a new Compiler. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/compile_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/compile_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/compile_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/compile_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/mapper.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/mapper.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/cel/mapper.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/cel/mapper.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/authenticated_group_adder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/authenticated_group_adder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/authenticated_group_adder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/authenticated_group_adder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/group_adder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/group_adder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/group_adder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/group_adder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/group_adder_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/group_adder_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/group_adder_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/group_adder_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/token_group_adder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/token_group_adder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/token_group_adder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/token_group_adder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/token_group_adder_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/token_group_adder_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/group/token_group_adder_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/group/token_group_adder_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/anonymous/anonymous.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/anonymous/anonymous.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/anonymous/anonymous.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/anonymous/anonymous.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/anonymous/anonymous_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/anonymous/anonymous_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/anonymous/anonymous_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/anonymous/anonymous_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/bearertoken/bearertoken.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/bearertoken/bearertoken.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/bearertoken/bearertoken.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/bearertoken/bearertoken.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/bearertoken/bearertoken_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/bearertoken/bearertoken_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/bearertoken/bearertoken_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/bearertoken/bearertoken_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_controller.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_controller.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_controller.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_controller.go index 38d6cbe71..e5419d0ce 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_controller.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_controller.go @@ -174,7 +174,7 @@ func (c *RequestHeaderAuthRequestController) AllowedClientNames() []string { // Run starts RequestHeaderAuthRequestController controller and blocks until stopCh is closed. func (c *RequestHeaderAuthRequestController) Run(ctx context.Context, workers int) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer c.queue.ShutDown() klog.Infof("Starting %s", c.name) @@ -183,7 +183,7 @@ func (c *RequestHeaderAuthRequestController) Run(ctx context.Context, workers in go c.configmapInformer.Run(ctx.Done()) // wait for caches to fill before starting your work - if !cache.WaitForNamedCacheSync(c.name, ctx.Done(), c.configmapInformerSynced) { + if !cache.WaitForNamedCacheSyncWithContext(ctx, c.configmapInformerSynced) { return } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_controller_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_controller_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_controller_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_controller_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/headerrequest/requestheader_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/headerrequest/requestheader_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/union/union.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/union/union.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/union/union.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/union/union.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/union/unionauth_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/union/unionauth_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/union/unionauth_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/union/unionauth_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/websocket/protocol.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/websocket/protocol.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/websocket/protocol.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/websocket/protocol.go index ee8c89f5c..a3a783315 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/websocket/protocol.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/websocket/protocol.go @@ -24,8 +24,8 @@ import ( "strings" "unicode/utf8" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/streaming/pkg/httpstream/wsstream" ) const bearerProtocolPrefix = "base64url.bearer.authorization.k8s.io." diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/websocket/protocol_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/websocket/protocol_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/websocket/protocol_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/websocket/protocol_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client-expired.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client-expired.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client-expired.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client-expired.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client-valid.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client-valid.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client-valid.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client-valid.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client.config.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client.config.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client.config.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client.config.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/client.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/client.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/generate.sh b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/generate.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/generate.sh rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/generate.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.config.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.config.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.config.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.config.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/intermediate.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/intermediate.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/root.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/root.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/root.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/root.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/root.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/root.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/testdata/root.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/testdata/root.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/verify_options.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/verify_options.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/verify_options.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/verify_options.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/x509.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/x509.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/x509.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/x509.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/x509_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/x509_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/request/x509/x509_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/request/x509/x509_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/serviceaccount/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/serviceaccount/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/serviceaccount/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/serviceaccount/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/serviceaccount/util_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/serviceaccount/util_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/serviceaccount/util_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/serviceaccount/util_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_simple.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_simple.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_simple.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_simple.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_striped.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_striped.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_striped.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_striped.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cache_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cache_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cached_token_authenticator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cached_token_authenticator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cached_token_authenticator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cached_token_authenticator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cached_token_authenticator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cached_token_authenticator_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/cached_token_authenticator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/cached_token_authenticator_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/stats.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/stats.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/cache/stats.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/cache/stats.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/jwt/jwt.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/jwt/jwt.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/jwt/jwt.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/jwt/jwt.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/tokenfile/tokenfile.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/tokenfile/tokenfile.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/tokenfile/tokenfile.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/tokenfile/tokenfile.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/tokenfile/tokenfile_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/tokenfile/tokenfile_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/tokenfile/tokenfile_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/tokenfile/tokenfile_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/union/union.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/union/union.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/union/union.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/union/union.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/union/unionauth_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/union/unionauth_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/token/union/unionauth_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/token/union/unionauth_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/user/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/user/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/user/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/user/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/user/user.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/user/user.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authentication/user/user.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authentication/user/user.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizer/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizer/interfaces.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizer/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizer/interfaces.go index 2f5f65e22..09000a1a5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizer/interfaces.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizer/interfaces.go @@ -18,6 +18,7 @@ package authorizer import ( "context" + "fmt" "net/http" "k8s.io/apimachinery/pkg/fields" @@ -182,3 +183,16 @@ const ( // to allow or deny an action. DecisionNoOpinion ) + +func (d Decision) String() string { + switch d { + case DecisionDeny: + return "Deny" + case DecisionAllow: + return "Allow" + case DecisionNoOpinion: + return "NoOpinion" + default: + return fmt.Sprintf("Unknown (%d)", int(d)) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizer/rule.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizer/rule.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizer/rule.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizer/rule.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/builtin.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/builtin.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/builtin.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/builtin.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/builtin_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/builtin_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/builtin_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/builtin_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/delegating.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/delegating.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/delegating.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/delegating.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/authorizerfactory/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/authorizerfactory/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/compile.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/compile.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/compile.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/compile.go index 765864846..04acda157 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/compile.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/compile.go @@ -68,7 +68,7 @@ type compiler struct { // NewDefaultCompiler returns a new Compiler following the default compatibility version. // Note: the compiler construction depends on feature gates and the compatibility version to be initialized. func NewDefaultCompiler() Compiler { - return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true)) + return NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) } // NewCompiler returns a new Compiler. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/compile_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/compile_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/compile_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/compile_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/matcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/matcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/cel/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/cel/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/path.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/path.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/path.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/path.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/path_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/path_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/path/path_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/path/path_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/union/union.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/union/union.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/union/union.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/union/union.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/union/union_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/union/union_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/authorization/union/union_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/authorization/union/union_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/cidr.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/cidr.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/cidr.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/cidr.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/adaptor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/adaptor.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/adaptor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/adaptor.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/equality.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/equality.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/equality.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/equality.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/equality_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/equality_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/equality_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/equality_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/maplist.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/maplist.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/maplist.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/maplist.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/schemas.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/schemas.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/schemas.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/schemas.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/typeprovider.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/typeprovider.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/typeprovider.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/typeprovider.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/typeprovider_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/typeprovider_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/typeprovider_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/typeprovider_test.go index 8e3d6e937..932b469b1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/typeprovider_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/typeprovider_test.go @@ -17,13 +17,14 @@ limitations under the License. package common import ( - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" "reflect" "strings" "testing" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/cel/environment" "k8s.io/apiserver/pkg/cel/mutation/dynamic" @@ -154,7 +155,7 @@ func (m *mockResolvedType) Val(fields map[string]ref.Val) ref.Val { // mustCreateEnv creates the default env for testing, with given option. // it fatally fails the test if the env fails to set up. func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { - envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). + envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()). Extend(environment.VersionedOptions{ IntroducedVersion: version.MajorMinor(1, 30), EnvOptions: envOptions, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/values_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/values_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/values_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/values_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/valuesreflect.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/valuesreflect.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/valuesreflect.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/valuesreflect.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/valuesunstructured.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/valuesunstructured.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/common/valuesunstructured.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/common/valuesunstructured.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base.go similarity index 89% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base.go index 4b0e7265f..91bdc6cb1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base.go @@ -57,9 +57,7 @@ func DefaultCompatibilityVersion() *version.Version { return effectiveVer.MinCompatibilityVersion() } -var baseOpts = append(baseOptsWithoutStrictCost, StrictCostOpt) - -var baseOptsWithoutStrictCost = []VersionedOptions{ +var baseOpts = []VersionedOptions{ { // CEL epoch was actually 1.23, but we artificially set it to 1.0 because these // options should always be present. @@ -190,6 +188,7 @@ var baseOptsWithoutStrictCost = []VersionedOptions{ ext.Lists(ext.ListsVersion(3)), }, }, + StrictCostOpt, } var ( @@ -223,7 +222,6 @@ var cacheBaseEnvs = true func DisableBaseEnvSetCachingForTests() { cacheBaseEnvs = false baseEnvs.Clear() - baseEnvsWithOption.Clear() } // MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics @@ -235,8 +233,7 @@ func DisableBaseEnvSetCachingForTests() { // The returned environment contains no CEL variable definitions or custom type declarations and // should be extended to construct environments with the appropriate variable definitions, // type declarations and any other needed configuration. -// strictCost is used to determine whether to enforce strict cost calculation for CEL expressions. -func MustBaseEnvSet(ver *version.Version, strictCost bool) *EnvSet { +func MustBaseEnvSet(ver *version.Version) *EnvSet { if ver == nil { panic("version must be non-nil") } @@ -245,38 +242,23 @@ func MustBaseEnvSet(ver *version.Version, strictCost bool) *EnvSet { } key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10) var entry interface{} - if strictCost { - if entry, ok := baseEnvs.Load(key); ok { - return entry.(*EnvSet) - } - entry, _, _ = baseEnvsSingleflight.Do(key, func() (interface{}, error) { - entry := mustNewEnvSet(ver, baseOpts) - if cacheBaseEnvs { - baseEnvs.Store(key, entry) - } - return entry, nil - }) - } else { - if entry, ok := baseEnvsWithOption.Load(key); ok { - return entry.(*EnvSet) - } - entry, _, _ = baseEnvsWithOptionSingleflight.Do(key, func() (interface{}, error) { - entry := mustNewEnvSet(ver, baseOptsWithoutStrictCost) - if cacheBaseEnvs { - baseEnvsWithOption.Store(key, entry) - } - return entry, nil - }) + if entry, ok := baseEnvs.Load(key); ok { + return entry.(*EnvSet) } + entry, _, _ = baseEnvsSingleflight.Do(key, func() (interface{}, error) { + entry := mustNewEnvSet(ver, baseOpts) + if cacheBaseEnvs { + baseEnvs.Store(key, entry) + } + return entry, nil + }) return entry.(*EnvSet) } var ( - baseEnvs = sync.Map{} - baseEnvsWithOption = sync.Map{} - baseEnvsSingleflight = &singleflight.Group{} - baseEnvsWithOptionSingleflight = &singleflight.Group{} + baseEnvs = sync.Map{} + baseEnvsSingleflight = &singleflight.Group{} ) // UnversionedLib wraps library initialization calls like ext.Sets() or library.IP() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base_test.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base_test.go index 2b68f6b26..a0644454a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/base_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/base_test.go @@ -32,10 +32,10 @@ import ( // a cached environment is loaded for each MustBaseEnvSet call. func BenchmarkLoadBaseEnv(b *testing.B) { ver := DefaultCompatibilityVersion() - MustBaseEnvSet(ver, true) + MustBaseEnvSet(ver) b.ResetTimer() for i := 0; i < b.N; i++ { - MustBaseEnvSet(ver, true) + MustBaseEnvSet(ver) } } @@ -44,7 +44,7 @@ func BenchmarkLoadBaseEnv(b *testing.B) { func BenchmarkLoadBaseEnvDifferentVersions(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - MustBaseEnvSet(version.MajorMinor(1, uint(i)), true) + MustBaseEnvSet(version.MajorMinor(1, uint(i))) } } @@ -124,7 +124,7 @@ func TestKnownLibraries(t *testing.T) { for _, lib := range library.KnownLibraries() { known.Insert(lib.LibraryName()) } - for _, libName := range MustBaseEnvSet(version.MajorMinor(1, 0), true).storedExpressions.Libraries() { + for _, libName := range MustBaseEnvSet(version.MajorMinor(1, 0)).storedExpressions.Libraries() { if strings.HasPrefix(libName, "cel.lib") { // ignore core libs continue } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/environment.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/environment.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/environment.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/environment.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/environment_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/environment_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/environment_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/environment_test.go index 6a1f917d3..db351850c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/environment/environment_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/environment/environment_test.go @@ -307,7 +307,7 @@ func TestBaseEnvironment(t *testing.T) { for _, tv := range tc.typeVersionCombinations { t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) { - envSet := MustBaseEnvSet(tv.version, true) + envSet := MustBaseEnvSet(tv.version) if tc.opts != nil { var err error envSet, err = envSet.Extend(tc.opts...) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/errors_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/errors_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/errors_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/errors_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/escaping.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/escaping.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/escaping.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/escaping.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/escaping_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/escaping_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/escaping_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/escaping_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/format.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/format.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/format.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/format.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/ip.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/ip.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/ip.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/ip.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/lazy/lazy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/lazy/lazy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/lazy/lazy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/lazy/lazy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/lazy/lazy_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/lazy/lazy_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/lazy/lazy_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/lazy/lazy_test.go index dffbb393d..01fc725e7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/lazy/lazy_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/lazy/lazy_test.go @@ -129,7 +129,7 @@ func compileAndRun(env *cel.Env, activation *testActivation, exp string) (ref.Va func buildTestEnv() (*cel.Env, *apiservercel.DeclType, error) { variablesType := apiservercel.NewMapType(apiservercel.StringType, apiservercel.AnyType, 0) variablesType.Fields = make(map[string]*apiservercel.DeclField) - envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend( + envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( environment.VersionedOptions{ IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{ diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/authz.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/authz.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/authz.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/authz.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cidr.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cidr.go similarity index 88% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cidr.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cidr.go index a638fd4ac..befc05d70 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cidr.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cidr.go @@ -82,24 +82,24 @@ import ( // // Examples: // -// cidr('192.168.0.0/24').containsIP(ip('192.168.0.1')) // returns true -// cidr('192.168.0.0/24').containsIP(ip('192.168.1.1')) // returns false -// cidr('192.168.0.0/24').containsIP('192.168.0.1') // returns true -// cidr('192.168.0.0/24').containsIP('192.168.1.1') // returns false -// cidr('192.168.0.0/16').containsCIDR(cidr('192.168.10.0/24')) // returns true -// cidr('192.168.1.0/24').containsCIDR(cidr('192.168.2.0/24')) // returns false -// cidr('192.168.0.0/16').containsCIDR('192.168.10.0/24') // returns true -// cidr('192.168.1.0/24').containsCIDR('192.168.2.0/24') // returns false -// cidr('192.168.0.1/24').ip() // returns ipAddr('192.168.0.1') -// cidr('192.168.0.1/24').ip().family() // returns '4' -// cidr('::1/128').ip() // returns ipAddr('::1') -// cidr('::1/128').ip().family() // returns '6' -// cidr('192.168.0.0/24').masked() // returns cidr('192.168.0.0/24') -// cidr('192.168.0.1/24').masked() // returns cidr('192.168.0.0/24') -// cidr('192.168.0.0/24') == cidr('192.168.0.0/24').masked() // returns true, CIDR was already in canonical format -// cidr('192.168.0.1/24') == cidr('192.168.0.1/24').masked() // returns false, CIDR was not in canonical format -// cidr('192.168.0.0/16').prefixLength() // returns 16 -// cidr('::1/128').prefixLength() // returns 128 +// cidr('192.168.0.0/24').containsIP(ip('192.168.0.1')) // returns true +// cidr('192.168.0.0/24').containsIP(ip('192.168.1.1')) // returns false +// cidr('192.168.0.0/24').containsIP('192.168.0.1') // returns true +// cidr('192.168.0.0/24').containsIP('192.168.1.1') // returns false +// cidr('192.168.0.0/16').containsCIDR(cidr('192.168.10.0/24')) // returns true +// cidr('192.168.1.0/24').containsCIDR(cidr('192.168.2.0/24')) // returns false +// cidr('192.168.0.0/16').containsCIDR('192.168.10.0/24') // returns true +// cidr('192.168.1.0/24').containsCIDR('192.168.2.0/24') // returns false +// cidr('192.168.0.1/24').ip() // returns ipAddr('192.168.0.1') +// cidr('192.168.0.1/24').ip().family() // returns '4' +// cidr('::1/128').ip() // returns ipAddr('::1') +// cidr('::1/128').ip().family() // returns '6' +// cidr('192.168.0.0/24').masked() // returns cidr('192.168.0.0/24') +// cidr('192.168.0.1/24').masked() // returns cidr('192.168.0.0/24') +// cidr('192.168.0.0/24') == cidr('192.168.0.0/24').masked() // returns true, CIDR was already in canonical format +// cidr('192.168.0.1/24') == cidr('192.168.0.1/24').masked() // returns false, CIDR was not in canonical format +// cidr('192.168.0.0/16').prefixLength() // returns 16 +// cidr('::1/128').prefixLength() // returns 128 func CIDR() cel.EnvOption { return cel.Lib(cidrsLib) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cidr_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cidr_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cidr_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cidr_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cost.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cost.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cost.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cost.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cost_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cost_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/cost_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/cost_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/format.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/format.go similarity index 81% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/format.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/format.go index 2adc35c90..e8b2d07b5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/format.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/format.go @@ -41,53 +41,53 @@ var ( // Format provides a CEL library exposing common named Kubernetes string // validations. Can be used in CRD ValidationRules messageExpression. // -// Example: +// Example: // -// rule: format.dns1123label.validate(object.metadata.name).hasValue() -// messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n") +// rule: format.dns1123label.validate(object.metadata.name).hasValue() +// messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n") // // format.named(name: string) -> ?Format // -// Returns the Format with the given name, if it exists. Otherwise, optional.none -// Allowed names are: -// - `dns1123Label` -// - `dns1123Subdomain` -// - `dns1035Label` -// - `qualifiedName` -// - `dns1123LabelPrefix` -// - `dns1123SubdomainPrefix` -// - `dns1035LabelPrefix` -// - `labelValue` -// - `uri` -// - `uuid` -// - `byte` -// - `date` -// - `datetime` +// Returns the Format with the given name, if it exists. Otherwise, optional.none +// Allowed names are: +// - `dns1123Label` +// - `dns1123Subdomain` +// - `dns1035Label` +// - `qualifiedName` +// - `dns1123LabelPrefix` +// - `dns1123SubdomainPrefix` +// - `dns1035LabelPrefix` +// - `labelValue` +// - `uri` +// - `uuid` +// - `byte` +// - `date` +// - `datetime` // // format.() -> Format // -// Convenience functions for all the named formats are also available +// Convenience functions for all the named formats are also available. // -// Examples: -// format.dns1123Label().validate("my-label-name") -// format.dns1123Subdomain().validate("apiextensions.k8s.io") -// format.dns1035Label().validate("my-label-name") -// format.qualifiedName().validate("apiextensions.k8s.io/v1beta1") -// format.dns1123LabelPrefix().validate("my-label-prefix-") -// format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-") -// format.dns1035LabelPrefix().validate("my-label-prefix-") -// format.uri().validate("http://example.com") -// Uses same pattern as isURL, but returns an error -// format.uuid().validate("123e4567-e89b-12d3-a456-426614174000") -// format.byte().validate("aGVsbG8=") -// format.date().validate("2021-01-01") -// format.datetime().validate("2021-01-01T00:00:00Z") +// Examples: +// +// format.dns1123Label().validate("my-label-name") +// format.dns1123Subdomain().validate("apiextensions.k8s.io") +// format.dns1035Label().validate("my-label-name") +// format.qualifiedName().validate("apiextensions.k8s.io/v1beta1") +// format.dns1123LabelPrefix().validate("my-label-prefix-") +// format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-") +// format.dns1035LabelPrefix().validate("my-label-prefix-") +// format.uri().validate("http://example.com") +// Uses same pattern as isURL, but returns an error +// format.uuid().validate("123e4567-e89b-12d3-a456-426614174000") +// format.byte().validate("aGVsbG8=") +// format.date().validate("2021-01-01") +// format.datetime().validate("2021-01-01T00:00:00Z") // - // .validate(str: string) -> ?list // -// Validates the given string against the given format. Returns optional.none -// if the string is valid, otherwise a list of validation error strings. +// Validates the given string against the given format. Returns optional.none +// if the string is valid, otherwise a list of validation error strings. func Format() cel.EnvOption { return cel.Lib(formatLib) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/format_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/format_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/format_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/format_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/ip.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/ip.go similarity index 87% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/ip.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/ip.go index 5a089ae1c..fa5e9f001 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/ip.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/ip.go @@ -99,30 +99,30 @@ import ( // // Examples: // -// ip('127.0.0.1').family() // returns '4” -// ip('::1').family() // returns '6' -// ip('127.0.0.1').family() == 4 // returns true -// ip('::1').family() == 6 // returns true -// ip('0.0.0.0').isUnspecified() // returns true -// ip('127.0.0.1').isUnspecified() // returns false -// ip('::').isUnspecified() // returns true -// ip('::1').isUnspecified() // returns false -// ip('127.0.0.1').isLoopback() // returns true -// ip('192.168.0.1').isLoopback() // returns false -// ip('::1').isLoopback() // returns true -// ip('2001:db8::abcd').isLoopback() // returns false -// ip('224.0.0.1').isLinkLocalMulticast() // returns true -// ip('224.0.1.1').isLinkLocalMulticast() // returns false -// ip('ff02::1').isLinkLocalMulticast() // returns true -// ip('fd00::1').isLinkLocalMulticast() // returns false -// ip('169.254.169.254').isLinkLocalUnicast() // returns true -// ip('192.168.0.1').isLinkLocalUnicast() // returns false -// ip('fe80::1').isLinkLocalUnicast() // returns true -// ip('fd80::1').isLinkLocalUnicast() // returns false -// ip('192.168.0.1').isGlobalUnicast() // returns true -// ip('255.255.255.255').isGlobalUnicast() // returns false -// ip('2001:db8::abcd').isGlobalUnicast() // returns true -// ip('ff00::1').isGlobalUnicast() // returns false +// ip('127.0.0.1').family() // returns '4” +// ip('::1').family() // returns '6' +// ip('127.0.0.1').family() == 4 // returns true +// ip('::1').family() == 6 // returns true +// ip('0.0.0.0').isUnspecified() // returns true +// ip('127.0.0.1').isUnspecified() // returns false +// ip('::').isUnspecified() // returns true +// ip('::1').isUnspecified() // returns false +// ip('127.0.0.1').isLoopback() // returns true +// ip('192.168.0.1').isLoopback() // returns false +// ip('::1').isLoopback() // returns true +// ip('2001:db8::abcd').isLoopback() // returns false +// ip('224.0.0.1').isLinkLocalMulticast() // returns true +// ip('224.0.1.1').isLinkLocalMulticast() // returns false +// ip('ff02::1').isLinkLocalMulticast() // returns true +// ip('fd00::1').isLinkLocalMulticast() // returns false +// ip('169.254.169.254').isLinkLocalUnicast() // returns true +// ip('192.168.0.1').isLinkLocalUnicast() // returns false +// ip('fe80::1').isLinkLocalUnicast() // returns true +// ip('fd80::1').isLinkLocalUnicast() // returns false +// ip('192.168.0.1').isGlobalUnicast() // returns true +// ip('255.255.255.255').isGlobalUnicast() // returns false +// ip('2001:db8::abcd').isGlobalUnicast() // returns true +// ip('ff00::1').isGlobalUnicast() // returns false func IP() cel.EnvOption { return cel.Lib(ipLib) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/ip_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/ip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/ip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/ip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/jsonpatch.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/jsonpatch.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/jsonpatch.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/jsonpatch.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/libraries.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/libraries.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/libraries.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/libraries.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/library_compatibility_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/library_compatibility_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/library_compatibility_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/library_compatibility_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/lists.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/lists.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/lists.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/lists.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/quantity.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/quantity.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/quantity.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/quantity.go index 236b366b4..f3fd8b1c5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/quantity.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/quantity.go @@ -70,7 +70,7 @@ import ( // // - asInteger: returns a representation of the current value as an int64 if // possible or results in an error if conversion would result in overflow -// or loss of precision. +// or loss of precision. // // - asApproximateFloat: returns a float64 representation of the quantity which may // lose precision. If the value of the quantity is outside the range of a float64 @@ -82,12 +82,12 @@ import ( // // Examples: // -// quantity("50000000G").isInteger() // returns true -// quantity("50k").isInteger() // returns true -// quantity("9999999999999999999999999999999999999G").asInteger() // error: cannot convert value to integer -// quantity("9999999999999999999999999999999999999G").isInteger() // returns false -// quantity("50k").asInteger() == 50000 // returns true -// quantity("50k").sub(20000).asApproximateFloat() == 30000 // returns true +// quantity("50000000G").isInteger() // returns true +// quantity("50k").isInteger() // returns true +// quantity("9999999999999999999999999999999999999G").asInteger() // error: cannot convert value to integer +// quantity("9999999999999999999999999999999999999G").isInteger() // returns false +// quantity("50k").asInteger() == 50000 // returns true +// quantity("50k").sub(20000).asApproximateFloat() == 30000 // returns true // // Arithmetic // @@ -105,11 +105,11 @@ import ( // // Examples: // -// quantity("50k").add("20k") == quantity("70k") // returns true -// quantity("50k").add(20) == quantity("50020") // returns true -// quantity("50k").sub("20k") == quantity("30k") // returns true -// quantity("50k").sub(20000) == quantity("30k") // returns true -// quantity("50k").add(20).sub(quantity("100k")).sub(-50000) == quantity("20") // returns true +// quantity("50k").add("20k") == quantity("70k") // returns true +// quantity("50k").add(20) == quantity("50020") // returns true +// quantity("50k").sub("20k") == quantity("30k") // returns true +// quantity("50k").sub(20000) == quantity("30k") // returns true +// quantity("50k").add(20).sub(quantity("100k")).sub(-50000) == quantity("20") // returns true // // Comparisons // @@ -119,21 +119,19 @@ import ( // // - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand // -// // .isLessThan() // .isGreaterThan() // .compareTo() // // Examples: // -// quantity("200M").compareTo(quantity("0.2G")) // returns 0 -// quantity("50M").compareTo(quantity("50Mi")) // returns -1 -// quantity("50Mi").compareTo(quantity("50M")) // returns 1 -// quantity("150Mi").isGreaterThan(quantity("100Mi")) // returns true -// quantity("50Mi").isGreaterThan(quantity("100Mi")) // returns false -// quantity("50M").isLessThan(quantity("100M")) // returns true -// quantity("100M").isLessThan(quantity("50M")) // returns false - +// quantity("200M").compareTo(quantity("0.2G")) // returns 0 +// quantity("50M").compareTo(quantity("50Mi")) // returns -1 +// quantity("50Mi").compareTo(quantity("50M")) // returns 1 +// quantity("150Mi").isGreaterThan(quantity("100Mi")) // returns true +// quantity("50Mi").isGreaterThan(quantity("100Mi")) // returns false +// quantity("50M").isLessThan(quantity("100M")) // returns true +// quantity("100M").isLessThan(quantity("50M")) // returns false func Quantity() cel.EnvOption { return cel.Lib(quantityLib) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/quantity_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/quantity_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/quantity_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/quantity_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/regex.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/regex.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/regex.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/regex.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/semver_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/semver_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/semver_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/semver_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/semverlib.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/semverlib.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/semverlib.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/semverlib.go index 93614f849..e7193c2a7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/semverlib.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/semverlib.go @@ -37,6 +37,7 @@ import ( // to semver.org documentation for information on accepted patterns. // An optional "normalize" argument can be passed to enable normalization. Normalization removes any "v" prefix, adds a // 0 minor and patch numbers to versions with only major or major.minor components specified, and removes any leading 0s. +// // semver() // semver(, ) // @@ -65,7 +66,7 @@ import ( // // isSemver('1.0.0') // returns true // isSemver('hello') // returns false -// isSemver('v1.0') // returns false (leading "v" is not allowed unless normalization is enabled) +// isSemver('v1.0') // returns false (leading "v" is not allowed unless normalization is enabled) // isSemver('v1.0', true) // Applies normalization to remove leading "v". returns true // semver('1.0', true) // Applies normalization to add the missing patch version. Returns true // semver('01.01.01', true) // Applies normalization to remove leading zeros. Returns true @@ -88,7 +89,6 @@ import ( // // - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand // -// // .isLessThan() // .isGreaterThan() // .compareTo() @@ -98,7 +98,6 @@ import ( // semver("1.2.3").compareTo(semver("1.2.3")) // returns 0 // semver("1.2.3").compareTo(semver("2.0.0")) // returns -1 // semver("1.2.3").compareTo(semver("0.1.2")) // returns 1 - func SemverLib(options ...SemverOption) cel.EnvOption { semverLib := &semverLibType{} for _, o := range options { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/urls.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/urls.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/library/urls.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/library/urls.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/limits.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/limits.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/limits.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/limits.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/dynamic/objects.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/dynamic/objects.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/dynamic/objects.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/dynamic/objects.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/dynamic/objects_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/dynamic/objects_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/dynamic/objects_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/dynamic/objects_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/jsonpatch.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/jsonpatch.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/jsonpatch.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/jsonpatch.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/typeresolver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/typeresolver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/typeresolver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/typeresolver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/typeresolver_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/typeresolver_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/typeresolver_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/typeresolver_test.go index 0338fb10f..f871e18a4 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/mutation/typeresolver_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/mutation/typeresolver_test.go @@ -340,7 +340,7 @@ func TestCELOptional(t *testing.T) { // mustCreateEnv creates the default env for testing, with given option. // it fatally fails the test if the env fails to set up. func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env { - envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true). + envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()). Extend(environment.VersionedOptions{ IntroducedVersion: version.MajorMinor(1, 0), // Always enabled. This is just for test. EnvOptions: envOptions, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/adaptor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/adaptor.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/adaptor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/adaptor.go index bc7b0d8c9..a5bfac262 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/adaptor.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/adaptor.go @@ -36,6 +36,9 @@ type SchemaOrBool struct { } func (sb *SchemaOrBool) Schema() common.Schema { + if sb.SchemaOrBool.Schema == nil { + return nil + } return &Schema{Schema: sb.SchemaOrBool.Schema} } @@ -153,7 +156,6 @@ func (s *Schema) Nullable() bool { func (s *Schema) AllOf() []common.Schema { var res []common.Schema for _, nestedSchema := range s.Schema.AllOf { - nestedSchema := nestedSchema res = append(res, &Schema{&nestedSchema}) } return res @@ -162,7 +164,6 @@ func (s *Schema) AllOf() []common.Schema { func (s *Schema) AnyOf() []common.Schema { var res []common.Schema for _, nestedSchema := range s.Schema.AnyOf { - nestedSchema := nestedSchema res = append(res, &Schema{&nestedSchema}) } return res @@ -171,7 +172,6 @@ func (s *Schema) AnyOf() []common.Schema { func (s *Schema) OneOf() []common.Schema { var res []common.Schema for _, nestedSchema := range s.Schema.OneOf { - nestedSchema := nestedSchema res = append(res, &Schema{&nestedSchema}) } return res diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/compiling_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/compiling_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/compiling_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/compiling_test.go index ca6db71a4..1799c2b0d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/compiling_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/compiling_test.go @@ -106,7 +106,7 @@ func buildTestEnv() (*cel.Env, error) { fooType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true).MaybeAssignTypeName("fooType") barType := common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true).MaybeAssignTypeName("barType") - env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true).Extend( + env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend( environment.VersionedOptions{ IntroducedVersion: version.MajorMinor(1, 26), EnvOptions: []cel.EnvOption{ diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/extensions.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/extensions.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/extensions.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/extensions.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/maplist_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/maplist_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/maplist_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/maplist_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/combined.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/combined.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/combined.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/combined.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/definitions.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/definitions.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/definitions.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/definitions.go index 12b353b0b..10b28a58b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/definitions.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/definitions.go @@ -40,9 +40,7 @@ type DefinitionsSchemaResolver struct { func NewDefinitionsSchemaResolver(getDefinitions common.GetOpenAPIDefinitions, schemes ...*runtime.Scheme) *DefinitionsSchemaResolver { gvkToRef := make(map[schema.GroupVersionKind]string) namer := openapi.NewDefinitionNamer(schemes...) - defs := getDefinitions(func(path string) spec.Ref { - return spec.MustCreateRef(path) - }) + defs := getDefinitions(spec.MustCreateRef) for name := range defs { _, e := namer.GetDefinitionName(name) gvks := extensionsToGVKs(e) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/discovery.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/discovery.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/discovery.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/discovery.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/refs.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/refs.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs.go index 56e2a4bbd..808d64341 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/refs.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs.go @@ -86,7 +86,9 @@ func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), visited sets.S } if populated != result.AdditionalProperties.Schema { changed = true - result.AdditionalProperties.Schema = populated + newProps := *result.AdditionalProperties + newProps.Schema = populated + result.AdditionalProperties = &newProps } } // schema is a list, populate its items @@ -97,7 +99,9 @@ func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), visited sets.S } if populated != result.Items.Schema { changed = true - result.Items.Schema = populated + newItems := *result.Items + newItems.Schema = populated + result.Items = &newItems } } if changed { diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs_test.go new file mode 100644 index 000000000..c2f0fc559 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/refs_test.go @@ -0,0 +1,124 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resolver + +import ( + "sync" + "testing" + + "github.com/go-openapi/jsonreference" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "k8s.io/kube-openapi/pkg/validation/spec" +) + +func schemaRef(ref string) *spec.Schema { + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef(ref), + }, + } +} + +func newSchema(ref string) *spec.Schema { + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + ID: ref, + }, + } +} + +// rootSchema returns a schema with references in all places where TestPopulateRefs +// resolves references. +func rootSchema() *spec.Schema { + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "a": *schemaRef("prop"), + }, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: schemaRef("additional"), + }, + Items: &spec.SchemaOrArray{ + Schema: schemaRef("item"), + }, + }, + } +} + +func resolvedRootSchema() *spec.Schema { + return &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Properties: map[string]spec.Schema{ + "a": *newSchema("prop"), + }, + AdditionalProperties: &spec.SchemaOrBool{ + Schema: newSchema("additional"), + }, + Items: &spec.SchemaOrArray{ + Schema: newSchema("item"), + }, + }, + } +} + +func TestPopulateRefs(t *testing.T) { + schemas := map[string]*spec.Schema{ + "root": rootSchema(), + } + + // Add one schema for each of the references above, with an ID that allows + // verifying that the right schema got resolved. + for _, ref := range []string{"prop", "additional", "item"} { + schemas[ref] = newSchema(ref) + } + + schemaOf := func(ref string) (*spec.Schema, bool) { + schema, ok := schemas[ref] + return schema, ok + } + + // Comparing the root schema below detects mutations where the data is semantically different, + // but it cannot detect undesired writes where the thing being written is the same (unlikely, + // but it could happen). Therefore we run two PopulateRefs calls and let the data race + // detector warn about concurrent, uncoordinated writes. + var wg sync.WaitGroup + var actualResolved *spec.Schema + wg.Go(func() { + schema, err := PopulateRefs(schemaOf, "root") + if err != nil { + t.Errorf("first PopulateRefs failed: %v", err) + } + actualResolved = schema + }) + wg.Go(func() { + _, err := PopulateRefs(schemaOf, "root") + if err != nil { + t.Errorf("second PopulateRefs failed: %v", err) + } + }) + wg.Wait() + + if diff := cmp.Diff(resolvedRootSchema(), actualResolved, cmpopts.IgnoreUnexported(jsonreference.Ref{})); diff != "" { + t.Errorf("unexpected resolved schema (- want, + got):\n%s", diff) + } + + if diff := cmp.Diff(rootSchema(), schemas["root"], cmpopts.IgnoreUnexported(jsonreference.Ref{})); diff != "" { + t.Errorf("read-only input schema got modified (- original, + modification):\n%s", diff) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/resolver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/resolver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/resolver/resolver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/resolver/resolver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/schemas_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/schemas_test.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/schemas_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/schemas_test.go index 5851a65f7..80499c54b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/schemas_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/schemas_test.go @@ -30,6 +30,33 @@ import ( ) func TestSchemaDeclType(t *testing.T) { + t.Run("a schema with additionalProperties: true should be an valid object type", func(t *testing.T) { + schema := &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "foo": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{Allows: true}, + }, + }, + }, + }, + } + declType := SchemaDeclType(schema, false) + if declType == nil { + t.Fatal("SchemaDeclType returned nil") + } + fooField, found := declType.FindField("foo") + if !found { + t.Fatal("field 'foo' not found") + } + fooType := fooField.Type + if fooType.TypeName() != "object" { + t.Fatalf("expected foo to be a object, but got %s", fooType.TypeName()) + } + }) ts := testSchema() cust := SchemaDeclType(ts, false) if cust.TypeName() != "object" { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/values_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/values_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/openapi/values_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/openapi/values_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/quantity.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/quantity.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/quantity.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/quantity.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/semver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/semver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/semver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/semver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/types_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/types_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/types_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/types_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/url.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/url.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/url.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/url.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/value.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/value.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/value.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/value.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/cel/value_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/cel/value_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/cel/value_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/cel/value_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/apiserver_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/apiserver_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/apiserver_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/apiserver_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/audit_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/audit_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/audit_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/audit_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/deprecation/deprecation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/deprecation/deprecation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/deprecation/deprecation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/deprecation/deprecation.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/deprecation/deprecation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/deprecation/deprecation_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/deprecation/deprecation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/deprecation/deprecation_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/addresses.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/addresses.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/addresses.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/addresses.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/addresses_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/addresses_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/addresses_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/addresses_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/etag.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/etag.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/etag.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/etag.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/fake.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/fake.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/fake.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/fake.go index 8cf0bb0b0..0cded481d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/fake.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/fake.go @@ -122,6 +122,18 @@ func (f *recorderResourceManager) SetGroupVersionPriority(gv metav1.GroupVersion }) } +func (f *recorderResourceManager) SetPeerDiscoveryProvider(provider PeerDiscoveryProvider) { + f.lock.Lock() + defer f.lock.Unlock() + + f.Actions = append(f.Actions, recorderResourceManagerAction{ + Type: "SetPeerDiscoveryProvider", + Group: "", + Version: "", + Value: provider, + }) +} + func (f *recorderResourceManager) AddGroupVersion(groupName string, value apidiscoveryv2.APIVersionDiscovery) { f.lock.Lock() defer f.lock.Unlock() @@ -173,3 +185,7 @@ func (f *recorderResourceManager) ServeHTTP(http.ResponseWriter, *http.Request) func (f *recorderResourceManager) WithSource(source Source) ResourceManager { panic("unimplemented") } + +func (f *recorderResourceManager) AddInvalidationCallback(callback func()) { + panic("unimplemented") +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler.go index 8b2d37d97..1b8e8c72e 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler.go @@ -85,6 +85,9 @@ type ResourceManager interface { // The group from the least-numbered source is used WithSource(source Source) ResourceManager + // AddInvalidationCallback adds a callback to be called when the discovery cache is invalidated. + AddInvalidationCallback(callback func()) + http.Handler } @@ -116,6 +119,10 @@ func (rm resourceManager) WithSource(source Source) ResourceManager { } } +func (rm resourceManager) AddInvalidationCallback(callback func()) { + rm.resourceDiscoveryManager.AddInvalidationCallback(callback) +} + type groupKey struct { name string @@ -138,9 +145,10 @@ type resourceDiscoveryManager struct { // Writes protected by the lock. // List of all apigroups & resources indexed by the resource manager - lock sync.RWMutex - apiGroups map[groupKey]*apidiscoveryv2.APIGroupDiscovery - versionPriorities map[groupVersionKey]priorityInfo + lock sync.RWMutex + apiGroups map[groupKey]*apidiscoveryv2.APIGroupDiscovery + versionPriorities map[groupVersionKey]priorityInfo + invalidationCallback atomic.Pointer[func()] } type priorityInfo struct { @@ -148,6 +156,17 @@ type priorityInfo struct { VersionPriority int } +func (rdm *resourceDiscoveryManager) AddInvalidationCallback(callback func()) { + rdm.invalidationCallback.Store(&callback) +} + +func (rdm *resourceDiscoveryManager) invalidateCacheLocked() { + rdm.cache.Store(nil) + if callback := rdm.invalidationCallback.Load(); callback != nil { + (*callback)() + } +} + func NewResourceManager(path string) ResourceManager { scheme := runtime.NewScheme() utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) @@ -188,7 +207,7 @@ func (rdm *resourceDiscoveryManager) SetGroupVersionPriority(source Source, gv m GroupPriorityMinimum: groupPriorityMinimum, VersionPriority: versionPriority, } - rdm.cache.Store(nil) + rdm.invalidateCacheLocked() } func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscoveryv2.APIGroupDiscovery) { @@ -196,7 +215,7 @@ func (rdm *resourceDiscoveryManager) SetGroups(source Source, groups []apidiscov defer rdm.lock.Unlock() rdm.apiGroups = nil - rdm.cache.Store(nil) + rdm.invalidateCacheLocked() for _, group := range groups { for _, version := range group.Versions { @@ -297,7 +316,7 @@ func (rdm *resourceDiscoveryManager) addGroupVersionLocked(source Source, groupN } // Reset response document so it is recreated lazily - rdm.cache.Store(nil) + rdm.invalidateCacheLocked() } func (rdm *resourceDiscoveryManager) RemoveGroupVersion(source Source, apiGroup metav1.GroupVersion) { @@ -338,7 +357,7 @@ func (rdm *resourceDiscoveryManager) RemoveGroupVersion(source Source, apiGroup } // Reset response document so it is recreated lazily - rdm.cache.Store(nil) + rdm.invalidateCacheLocked() } func (rdm *resourceDiscoveryManager) RemoveGroup(source Source, groupName string) { @@ -490,7 +509,7 @@ func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList { } etag, err := calculateETag(response) if err != nil { - klog.Errorf("failed to calculate etag for discovery document: %s", etag) + klog.Errorf("failed to calculate etag for discovery document: %v", err) etag = "" } cached := &cachedGroupList{ @@ -521,53 +540,66 @@ func (rdm *resourceDiscoveryManager) serveHTTP(resp http.ResponseWriter, req *ht response := cache.cachedResponse etag := cache.cachedResponseETag - mediaType, _, err := negotiation.NegotiateOutputMediaType(req, rdm.serializer, DiscoveryEndpointRestrictions) + writeDiscoveryResponse(&response, etag, rdm.serializer, resp, req) +} + +func writeDiscoveryResponse( + resp *apidiscoveryv2.APIGroupDiscoveryList, + etag string, + serializer runtime.NegotiatedSerializer, + w http.ResponseWriter, + req *http.Request, +) { + mediaType, _, err := negotiation.NegotiateOutputMediaType(req, serializer, DiscoveryEndpointRestrictions) if err != nil { // Should never happen. wrapper.go will only proxy requests to this // handler if the media type passes DiscoveryEndpointRestrictions utilruntime.HandleError(err) - resp.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) return } var targetGV schema.GroupVersion - if mediaType.Convert == nil || - (mediaType.Convert.GroupVersion() != apidiscoveryv2.SchemeGroupVersion && - mediaType.Convert.GroupVersion() != apidiscoveryv2beta1.SchemeGroupVersion) { + if mediaType.Convert == nil { + utilruntime.HandleError(fmt.Errorf("expected aggregated discovery group version, got unknown group and version")) + w.WriteHeader(http.StatusInternalServerError) + return + } + if mediaType.Convert.GroupVersion() != apidiscoveryv2.SchemeGroupVersion && + mediaType.Convert.GroupVersion() != apidiscoveryv2beta1.SchemeGroupVersion { utilruntime.HandleError(fmt.Errorf("expected aggregated discovery group version, got group: %s, version %s", mediaType.Convert.Group, mediaType.Convert.Version)) - resp.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) return } if mediaType.Convert.GroupVersion() == apidiscoveryv2beta1.SchemeGroupVersion && utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryRemoveBetaType) { klog.Errorf("aggregated discovery version v2beta1 is removed. Please update to use v2") - resp.WriteHeader(http.StatusNotFound) + w.WriteHeader(http.StatusNotFound) return } - targetGV = mediaType.Convert.GroupVersion() if len(etag) > 0 { // Use proper e-tag headers if one is available ServeHTTPWithETag( - &response, + resp, etag, targetGV, - rdm.serializer, - resp, + serializer, + w, req, ) } else { // Default to normal response in rare case etag is // not cached with the object for some reason. responsewriters.WriteObjectNegotiated( - rdm.serializer, + serializer, DiscoveryEndpointRestrictions, targetGV, - resp, + w, req, http.StatusOK, - &response, + resp, true, ) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler_test.go index 4eb8e123b..f707808f1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/handler_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/handler_test.go @@ -39,6 +39,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + utilversion "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/version" apidiscoveryv2conversion "k8s.io/apiserver/pkg/apis/apidiscovery/v2" discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" @@ -133,7 +134,7 @@ func fetchPath(handler http.Handler, acceptPrefix string, path string, etag stri func fetchPathHelper(handler http.Handler, accept string, path string, etag string) (*http.Response, []byte) { // Expect json-formatted apis group list w := httptest.NewRecorder() - req := httptest.NewRequest(request.MethodGet, discoveryPath, nil) + req := httptest.NewRequest(request.MethodGet, path, nil) // Ask for JSON response req.Header.Set("Accept", accept) @@ -190,6 +191,7 @@ func TestBasicResponseProtobuf(t *testing.T) { // V2Beta1 should still be served func TestV2Beta1SkewSupport(t *testing.T) { + featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, utilversion.MustParse("1.34")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryRemoveBetaType, false) manager := discoveryendpoint.NewResourceManager("apis") diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics.go new file mode 100644 index 000000000..a94cd588b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics.go @@ -0,0 +1,73 @@ +/* +Copyright 2023 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 aggregated + +import ( + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +const subsystem = "aggregator_discovery" + +var ( + regenerationCounter = metrics.NewCounter( + &metrics.CounterOpts{ + Name: "aggregation_count_total", + Subsystem: subsystem, + Help: "Counter of number of times discovery was aggregated", + StabilityLevel: metrics.ALPHA, + }, + ) + + PeerAggregatedCacheHitsCounter = metrics.NewCounter( + &metrics.CounterOpts{ + Name: "peer_aggregated_cache_hits_total", + Subsystem: subsystem, + Help: "Counter of number of times discovery was served from peer-aggregated cache", + StabilityLevel: metrics.ALPHA, + }, + ) + + PeerAggregatedCacheMissesCounter = metrics.NewCounter( + &metrics.CounterOpts{ + Name: "peer_aggregated_cache_misses_total", + Subsystem: subsystem, + Help: "Counter of number of times discovery was aggregated across all API servers", + StabilityLevel: metrics.ALPHA, + }, + ) + + NoPeerDiscoveryRequestCounter = metrics.NewCounter( + &metrics.CounterOpts{ + Name: "nopeer_requests_total", + Subsystem: subsystem, + Help: "Counter of number of times no-peer (non peer-aggregated) discovery was requested", + StabilityLevel: metrics.ALPHA, + }, + ) +) + +func init() { + legacyregistry.MustRegister(regenerationCounter) + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.UnknownVersionInteroperabilityProxy) { + legacyregistry.MustRegister(PeerAggregatedCacheHitsCounter) + legacyregistry.MustRegister(PeerAggregatedCacheMissesCounter) + legacyregistry.MustRegister(NoPeerDiscoveryRequestCounter) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics_test.go new file mode 100644 index 000000000..93565eb0a --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/metrics_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2022 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 aggregated_test + +import ( + "fmt" + "io" + "strings" + "testing" + + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func formatExpectedMetrics(aggregationCount int) io.Reader { + expected := `` + if aggregationCount > 0 { + expected = expected + `# HELP aggregator_discovery_aggregation_count_total [ALPHA] Counter of number of times discovery was aggregated +# TYPE aggregator_discovery_aggregation_count_total counter +aggregator_discovery_aggregation_count_total %d +` + } + args := []any{} + if aggregationCount > 0 { + args = append(args, aggregationCount) + } + return strings.NewReader(fmt.Sprintf(expected, args...)) +} + +func TestBasicMetrics(t *testing.T) { + legacyregistry.Reset() + manager := discoveryendpoint.NewResourceManager("apis") + + apis := fuzzAPIGroups(1, 3, 10) + manager.SetGroups(apis.Items) + + interests := []string{"aggregator_discovery_aggregation_count_total"} + + _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") + // A single fetch should aggregate and increment regeneration counter. + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { + t.Fatal(err) + } + _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") + // Subsequent fetches should not reaggregate discovery. + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { + t.Fatal(err) + } +} + +func TestMetricsModified(t *testing.T) { + legacyregistry.Reset() + manager := discoveryendpoint.NewResourceManager("apis") + + apis := fuzzAPIGroups(1, 3, 10) + manager.SetGroups(apis.Items) + + interests := []string{"aggregator_discovery_aggregation_count_total"} + + _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") + // A single fetch should aggregate and increment regeneration counter. + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(1), interests...); err != nil { + t.Fatal(err) + } + + // Update discovery document. + manager.SetGroups(fuzzAPIGroups(1, 3, 10).Items) + _, _, _ = fetchPath(manager, "application/json", discoveryPath, "") + // If the discovery content has changed, reaggregation should be performed. + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, formatExpectedMetrics(2), interests...); err != nil { + t.Fatal(err) + } +} + +func TestPeerAggregatedDiscoveryMetrics(t *testing.T) { + legacyregistry.Reset() + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.UnknownVersionInteroperabilityProxy, true) + manager := discoveryendpoint.NewResourceManager("apis") + localGroup := newAPIGroup("local.example.com", "v1", "local-resource") + manager.AddGroupVersion(localGroup.Name, localGroup.Versions[0]) + peerProvider := &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer.example.com", "v2", "peer-resource"), + }, + }, + } + peerAggregatedDiscoveryManager := discoveryendpoint.NewPeerAggregatedDiscoveryHandler("test-server", manager, peerProvider, "apis") + wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(manager, manager, peerAggregatedDiscoveryManager) + + _ = legacyregistry.Register(discoveryendpoint.PeerAggregatedCacheHitsCounter) + _ = legacyregistry.Register(discoveryendpoint.PeerAggregatedCacheMissesCounter) + _ = legacyregistry.Register(discoveryendpoint.NoPeerDiscoveryRequestCounter) + + // Make 3 peer-aggregated requests. + fetchPath(wrapped, "application/json", "/apis", "") + fetchPath(wrapped, "application/json;profile=foo", "/apis", "") + fetchPath(wrapped, "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", "/apis", "") + + // Make 2 no-peer discovery requests. + fetchPath(wrapped, "application/json;profile=nopeer", "/apis", "") + peerAggregatedDiscoveryManager.InvalidateCache() + fetchPath(wrapped, "application/json;profile=nopeer", "/apis", "") + peerAggregatedDiscoveryManager.InvalidateCache() + + cacheHitsTotalMetric := ` +# HELP aggregator_discovery_peer_aggregated_cache_hits_total [ALPHA] Counter of number of times discovery was served from peer-aggregated cache +# TYPE aggregator_discovery_peer_aggregated_cache_hits_total counter +aggregator_discovery_peer_aggregated_cache_hits_total 2 +` + + cacheMissesTotalMetric := ` +# HELP aggregator_discovery_peer_aggregated_cache_misses_total [ALPHA] Counter of number of times discovery was aggregated across all API servers +# TYPE aggregator_discovery_peer_aggregated_cache_misses_total counter +aggregator_discovery_peer_aggregated_cache_misses_total 1 +` + + noPeerDiscoveryTotalMetric := ` +# HELP aggregator_discovery_nopeer_requests_total [ALPHA] Counter of number of times no-peer (non peer-aggregated) discovery was requested +# TYPE aggregator_discovery_nopeer_requests_total counter +aggregator_discovery_nopeer_requests_total 2 +` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(cacheHitsTotalMetric), "aggregator_discovery_peer_aggregated_cache_hits_total"); err != nil { + t.Errorf("unexpected metrics output: %v", err) + } + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(cacheMissesTotalMetric), "aggregator_discovery_peer_aggregated_cache_misses_total"); err != nil { + t.Errorf("unexpected metrics output: %v", err) + } + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(noPeerDiscoveryTotalMetric), "aggregator_discovery_nopeer_requests_total"); err != nil { + t.Errorf("unexpected metrics output: %v", err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/negotiation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/negotiation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/negotiation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/negotiation.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler.go new file mode 100644 index 000000000..c7f54376c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler.go @@ -0,0 +1,495 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aggregated + +import ( + "net/http" + "reflect" + "sort" + "sync/atomic" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/klog/v2" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + utilsort "k8s.io/apimachinery/pkg/util/sort" +) + +// PeerDiscoveryProvider defines an interface to get peer resources for peer-aggregated discovery. +type PeerDiscoveryProvider interface { + GetPeerResources() map[string][]apidiscoveryv2.APIGroupDiscovery +} + +// PeerAggregatedResourceManager defines the interface for managing peer-aggregated discovery resources +// that combines both local and peer server resources. +type PeerAggregatedResourceManager interface { + // InvalidateCache invalidates the peer-aggregated discovery caches + // This should be called when peer discovery data changes. + InvalidateCache() + + // ServeHTTP handles peer-aggregated discovery HTTP requests. + http.Handler +} + +type peerAggregatedDiscoveryHandler struct { + serverID string + localResourceManager ResourceManager + peerDiscoveryProvider PeerDiscoveryProvider + serializer runtime.NegotiatedSerializer + cache atomic.Pointer[cachedGroupList] + serveHTTPFunc func(http.ResponseWriter, *http.Request) +} + +// NewPeerAggregatedDiscoveryHandler creates a new handler for peer-aggregated discovery. +func NewPeerAggregatedDiscoveryHandler(serverID string, localDiscoveryProvider ResourceManager, peerDiscoveryProvider PeerDiscoveryProvider, path string) PeerAggregatedResourceManager { + scheme := runtime.NewScheme() + utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) + codecs := serializer.NewCodecFactory(scheme) + + h := &peerAggregatedDiscoveryHandler{ + serverID: serverID, + localResourceManager: localDiscoveryProvider, + peerDiscoveryProvider: peerDiscoveryProvider, + serializer: codecs, + } + + h.localResourceManager.AddInvalidationCallback(func() { + h.InvalidateCache() + }) + + // Instrumentation wrapper for serveHTTP + h.serveHTTPFunc = metrics.InstrumentHandlerFunc(request.MethodGet, + "", "", "", path, "", metrics.APIServerComponent, false, "", + h.serveHTTP) + + return h +} + +// InvalidateCache invalidates the peer-aggregated discovery caches. +// This should be called when peer discovery data changes. +func (h *peerAggregatedDiscoveryHandler) InvalidateCache() { + h.cache.Store(nil) + klog.V(4).Info("Invalidated peer-aggregated discovery caches") +} + +func (h *peerAggregatedDiscoveryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + h.serveHTTPFunc(resp, req) +} + +func (h *peerAggregatedDiscoveryHandler) serveHTTP(resp http.ResponseWriter, req *http.Request) { + cache := h.fetchFromCache() + response := cache.cachedResponse + etag := cache.cachedResponseETag + + writeDiscoveryResponse(&response, etag, h.serializer, resp, req) +} + +func (h *peerAggregatedDiscoveryHandler) fetchFromCache() *cachedGroupList { + cacheLoad := h.cache.Load() + if cacheLoad != nil { + PeerAggregatedCacheHitsCounter.Inc() + return cacheLoad + } + + PeerAggregatedCacheMissesCounter.Inc() + + // Get local groups + var localGroups []apidiscoveryv2.APIGroupDiscovery + if rdm, ok := h.localResourceManager.(resourceManager); ok { + localCache := rdm.fetchFromCache() + if localCache != nil { + localGroups = localCache.cachedResponse.Items + } + } + + // Get peer resources if provider is set + var mergedGroups []apidiscoveryv2.APIGroupDiscovery + if h.peerDiscoveryProvider != nil { + peerResources := h.peerDiscoveryProvider.GetPeerResources() + mergedGroups = h.mergeResources(localGroups, peerResources) + } else { + mergedGroups = localGroups + } + + response := apidiscoveryv2.APIGroupDiscoveryList{ + Items: mergedGroups, + } + etag, err := calculateETag(response) + if err != nil { + klog.Errorf("failed to calculate etag for discovery document: %v", err) + etag = "" + } + + cached := &cachedGroupList{ + cachedResponse: response, + cachedResponseETag: etag, + } + h.cache.Store(cached) + return cached +} + +// mergeResources merges local and peer APIGroupDiscovery lists, preserving relative order as much as possible. +// localGroups is a list of APIGroupDiscovery objects from the local server. +// peerGroupDiscovery is a map of peer server IDs to their APIGroupDiscovery lists. +func (h *peerAggregatedDiscoveryHandler) mergeResources( + localGroups []apidiscoveryv2.APIGroupDiscovery, + peerGroupDiscovery map[string][]apidiscoveryv2.APIGroupDiscovery, +) []apidiscoveryv2.APIGroupDiscovery { + discoveryByServerID := h.combineServerDiscoveryLists(localGroups, peerGroupDiscovery) + if len(discoveryByServerID) <= 1 { + // No merging or sorting needed. + return localGroups + } + + mergedGroupMap := make(map[string]apidiscoveryv2.APIGroupDiscovery) + allGroupNames := make([][]string, 0, len(discoveryByServerID)) + sortedServerIDs := sortServerIDs(discoveryByServerID) + + // contentChanged tracks if any new groups/versions/resources were added in a peer. + contentChanged := false + // groupNameListsAreIdentical tracks if all peers have the exact same group names as local. + groupNameListsAreIdentical := true + localGroupNames := localGroupNames(localGroups) + for _, serverID := range sortedServerIDs { + discoveryGroups := discoveryByServerID[serverID] + groupNames, didThisServerMerge := h.processServerGroups(discoveryGroups, mergedGroupMap) + if didThisServerMerge { + contentChanged = true + } + allGroupNames = append(allGroupNames, groupNames) + // Check if this server's group order differs from local. + // 1. It catches simple re-orderings (e.g., ["g1", "g2"] vs ["g2", "g1"]). + // 2. It catches new or removed groups (e.g., ["g1"] vs ["g1", "g2"]). + // In either case, the list is not identical, and we must fall through + // to the full merge to ensure a deterministic result. + if !reflect.DeepEqual(localGroupNames, groupNames) { + groupNameListsAreIdentical = false + } + } + + // Case 1: Total short-circuit. + // Nothing changed (no new content AND no order change). + if !contentChanged && groupNameListsAreIdentical { + return localGroups + } + + var finalGroupOrder []string + // Case 2: Content changed, but group order didn't. + // We must return the new content, but we can skip the expensive sort. + if contentChanged && groupNameListsAreIdentical { + finalGroupOrder = localGroupNames + } else { + // Case 3: Group lists were different (new groups, different order, or both). + // We have no choice but to run the full topological sort. + finalGroupOrder = utilsort.MergePreservingRelativeOrder(allGroupNames) + } + + return assembleGroups(mergedGroupMap, finalGroupOrder) +} + +func (h *peerAggregatedDiscoveryHandler) processServerGroups( + discoveryGroups []apidiscoveryv2.APIGroupDiscovery, + mergedGroupMap map[string]apidiscoveryv2.APIGroupDiscovery, +) (groupNames []string, contentMerged bool) { + groupNames = make([]string, 0, len(discoveryGroups)) + contentMerged = false + + for _, group := range discoveryGroups { + if existing, ok := mergedGroupMap[group.Name]; ok { + // Merge versions and resources. + mergedG, didMerge := mergeVersionsAcrossGroup(existing, group) + mergedGroupMap[group.Name] = mergedG + if didMerge { + contentMerged = true + } + } else { + mergedGroupMap[group.Name] = group + } + groupNames = append(groupNames, group.Name) + } + + return groupNames, contentMerged +} + +// mergeVersionsAcrossGroup merges two APIGroupDiscovery objects. +// It returns a new merged object and a boolean indicating if the result differs from a. +// The result may differ if any new versions/resources were added or if the order was different. +func mergeVersionsAcrossGroup(a, b apidiscoveryv2.APIGroupDiscovery) (apidiscoveryv2.APIGroupDiscovery, bool) { + versionOrder := [][]string{} + mergedVersionMap := make(map[string]apidiscoveryv2.APIVersionDiscovery) + + aVersionNames := make([]string, 0, len(a.Versions)) + bVersionNames := make([]string, 0, len(b.Versions)) + // This flag tracks if 'b' modified existing versions. + contentMerged := false + + for _, v := range a.Versions { + aVersionNames = append(aVersionNames, v.Version) + mergedVersionMap[v.Version] = v + } + versionOrder = append(versionOrder, aVersionNames) + + for _, v := range b.Versions { + bVersionNames = append(bVersionNames, v.Version) + if existing, ok := mergedVersionMap[v.Version]; !ok { + mergedVersionMap[v.Version] = v + } else { + // Version exists in both, must merge their resources + mergedV, didMerge := mergeResourcesAcrossVersion(existing, v) + mergedVersionMap[v.Version] = mergedV + if didMerge { + contentMerged = true + } + } + } + versionOrder = append(versionOrder, bVersionNames) + + // Case 1: Total short-circuit. + // No new versions/resources were added and order is the same. + versionNamesAreIdentical := reflect.DeepEqual(aVersionNames, bVersionNames) + if !contentMerged && versionNamesAreIdentical { + return a, false + } + + var finalVersionOrder []string + if versionNamesAreIdentical { + // Case 2: Content changed, but version order didn't. + // We can skip the expensive sort and just use the known order. + finalVersionOrder = aVersionNames + } else { + // Case 3: Version lists were different (new versions, different order, or both). + // Must perform topological sort. + finalVersionOrder = utilsort.MergePreservingRelativeOrder(versionOrder) + } + + mergedVersions := orderedAPIVersionList(mergedVersionMap, finalVersionOrder) + return apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: *a.ObjectMeta.DeepCopy(), + TypeMeta: a.TypeMeta, + Versions: mergedVersions, + }, true +} + +// mergeResourcesAcrossVersion merges two APIVersionDiscovery objects. +// It returns a new merged object and a boolean indicating if the result differs from a. +// The result may differ if any new resources were added or if the order was different. +func mergeResourcesAcrossVersion(a, b apidiscoveryv2.APIVersionDiscovery) (apidiscoveryv2.APIVersionDiscovery, bool) { + resourceOrder := [][]string{} + mergedResourceMap := make(map[string]apidiscoveryv2.APIResourceDiscovery) + + aResourceNames := make([]string, 0, len(a.Resources)) + bResourceNames := make([]string, 0, len(b.Resources)) + // This flag tracks if 'b' modified existing resources. + contentChanged := false + + for _, r := range a.Resources { + aResourceNames = append(aResourceNames, r.Resource) + mergedResourceMap[r.Resource] = r + } + resourceOrder = append(resourceOrder, aResourceNames) + + for _, r := range b.Resources { + bResourceNames = append(bResourceNames, r.Resource) + if existing, ok := mergedResourceMap[r.Resource]; !ok { + mergedResourceMap[r.Resource] = r + } else { + if reflect.DeepEqual(existing, r) { + continue + } + contentChanged = true + mergedR := mergeResourceCapabilities(existing, r) + mergedResourceMap[r.Resource] = mergedR + } + } + resourceOrder = append(resourceOrder, bResourceNames) + + // Case 1: Total short-circuit. + // 'b' didn't add any new resources AND the resource order is the same. + resourceNameListIsIdentical := reflect.DeepEqual(aResourceNames, bResourceNames) + if !contentChanged && resourceNameListIsIdentical { + return a, false + } + + var finalResourceOrder []string + if resourceNameListIsIdentical { + // Case 2: Content changed, but resource order didn't. + // We can skip the expensive sort and just use the known order. + finalResourceOrder = aResourceNames + } else { + // Case 3: Resource lists were different (new resources, different order, or both). + // Must perform topological sort. + finalResourceOrder = utilsort.MergePreservingRelativeOrder(resourceOrder) + } + + mergedResources := orderedAPIResourceList(mergedResourceMap, finalResourceOrder) + return apidiscoveryv2.APIVersionDiscovery{ + Version: a.Version, + Resources: mergedResources, + Freshness: a.Freshness, + }, true +} + +// mergeResourceCapabilities takes two resources and returns a new one +// containing the union of their verbs and subresources. +// The resulting slices are sorted to ensure the merge is deterministic. +func mergeResourceCapabilities(a, b apidiscoveryv2.APIResourceDiscovery) apidiscoveryv2.APIResourceDiscovery { + // 1. Merge Verbs + verbSet := make(map[string]struct{}) + for _, v := range a.Verbs { + verbSet[v] = struct{}{} + } + for _, v := range b.Verbs { + verbSet[v] = struct{}{} + } + + var newVerbs []string + if len(verbSet) > 0 { + newVerbs = make([]string, 0, len(verbSet)) + for v := range verbSet { + newVerbs = append(newVerbs, v) + } + // Sort for deterministic ETag + sort.Strings(newVerbs) + } + + // 2. Merge Subresources + subresourceSet := make(map[string]apidiscoveryv2.APISubresourceDiscovery) + for _, s := range a.Subresources { + subresourceSet[s.Subresource] = s + } + for _, s := range b.Subresources { + subresourceSet[s.Subresource] = s + } + + var newSubresources []apidiscoveryv2.APISubresourceDiscovery + if len(subresourceSet) > 0 { + newSubresources = make([]apidiscoveryv2.APISubresourceDiscovery, 0, len(subresourceSet)) + for _, s := range subresourceSet { + newSubresources = append(newSubresources, s) + } + // Sort for deterministic ETag + sort.Slice(newSubresources, func(i, j int) bool { + return newSubresources[i].Subresource < newSubresources[j].Subresource + }) + } + + // 3. Return the new, merged object + // We prefer 'a's metadata (like ResponseKind) but 'b's could be used if 'a' is empty. + mergedResource := a + if len(mergedResource.Resource) == 0 { + mergedResource = b + } + mergedResource.Verbs = newVerbs + mergedResource.Subresources = newSubresources + + return mergedResource +} + +// assembleGroups builds the final slice from the merged map and ordered list. +func assembleGroups( + groupMap map[string]apidiscoveryv2.APIGroupDiscovery, + orderedGroups []string, +) []apidiscoveryv2.APIGroupDiscovery { + result := make([]apidiscoveryv2.APIGroupDiscovery, 0, len(orderedGroups)) + for _, groupName := range orderedGroups { + if group, ok := groupMap[groupName]; ok { + result = append(result, group) + } + } + return result +} + +// orderedAPIVersionList builds the final APIGroupDiscovery struct. +func orderedAPIVersionList( + versionMap map[string]apidiscoveryv2.APIVersionDiscovery, + orderedVersions []string, +) []apidiscoveryv2.APIVersionDiscovery { + mergedVersions := make([]apidiscoveryv2.APIVersionDiscovery, 0, len(orderedVersions)) + for _, vName := range orderedVersions { + if v, ok := versionMap[vName]; ok { + mergedVersions = append(mergedVersions, v) + delete(versionMap, vName) // Avoid duplicates + } + } + + return mergedVersions +} + +// orderedAPIResourceList builds the final APIVersionDiscovery struct. +func orderedAPIResourceList( + resourceMap map[string]apidiscoveryv2.APIResourceDiscovery, + orderedResources []string, +) []apidiscoveryv2.APIResourceDiscovery { + mergedResources := make([]apidiscoveryv2.APIResourceDiscovery, 0, len(orderedResources)) + for _, rName := range orderedResources { + if r, ok := resourceMap[rName]; ok { + mergedResources = append(mergedResources, r) + delete(resourceMap, rName) + } + } + + return mergedResources +} + +func (h *peerAggregatedDiscoveryHandler) combineServerDiscoveryLists(localGroups []apidiscoveryv2.APIGroupDiscovery, peerGroupDiscovery map[string][]apidiscoveryv2.APIGroupDiscovery, +) map[string][]apidiscoveryv2.APIGroupDiscovery { + allServerLists := make(map[string][]apidiscoveryv2.APIGroupDiscovery, len(peerGroupDiscovery)+1) + allServerLists[h.serverID] = localGroups + for peerID, peerList := range peerGroupDiscovery { + allServerLists[peerID] = peerList + } + return allServerLists +} + +func sortServerIDs(allServerLists map[string][]apidiscoveryv2.APIGroupDiscovery) []string { + allServerIDs := make([]string, 0, len(allServerLists)) + for serverID := range allServerLists { + allServerIDs = append(allServerIDs, serverID) + } + sort.Strings(allServerIDs) + return allServerIDs +} + +func localGroupNames(localGroups []apidiscoveryv2.APIGroupDiscovery) []string { + localGroupNames := make([]string, 0, len(localGroups)) + for _, g := range localGroups { + localGroupNames = append(localGroupNames, g.Name) + } + return localGroupNames +} + +// TestMergeResources is for testing only. It allows tests to call MergeResources without exporting peerAggregatedDiscoveryHandler. +func TestMergeResources(serverID string, localGroups []apidiscoveryv2.APIGroupDiscovery, peerGroupDiscovery map[string][]apidiscoveryv2.APIGroupDiscovery) []apidiscoveryv2.APIGroupDiscovery { + h := &peerAggregatedDiscoveryHandler{serverID: serverID} + return h.mergeResources(localGroups, peerGroupDiscovery) +} + +// TestFetchFromCache is for testing only. It allows tests to call fetchFromCache without exporting peerAggregatedDiscoveryHandler. +// Returns the cached response and ETag. +func TestFetchFromCache(serverID string, localResourceManager ResourceManager, peerDiscoveryProvider PeerDiscoveryProvider) (apidiscoveryv2.APIGroupDiscoveryList, string) { + h := &peerAggregatedDiscoveryHandler{ + serverID: serverID, + localResourceManager: localResourceManager, + peerDiscoveryProvider: peerDiscoveryProvider, + } + cached := h.fetchFromCache() + return cached.cachedResponse, cached.cachedResponseETag +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler_test.go new file mode 100644 index 000000000..9dcf34e37 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/peer_aggregated_handler_test.go @@ -0,0 +1,490 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aggregated_test + +import ( + "net/http" + "reflect" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "k8s.io/apimachinery/pkg/runtime/schema" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" +) + +func TestPeerAggregatedDiscovery(t *testing.T) { + manager := discoveryendpoint.NewResourceManager("apis") + localGroup := newAPIGroup("local.example.com", "v1", "local-resource") + manager.AddGroupVersion(localGroup.Name, localGroup.Versions[0]) + + peerProvider := &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer.example.com", "v2", "peer-resource"), + }, + }, + } + + testCases := []struct { + name string + enablePeerAggregatedDiscovery bool + acceptHeader string + peerProvider discoveryendpoint.PeerDiscoveryProvider + wantGroupNames []string + }{ + { + name: "Peer aggregated discovery disabled (should get local discovery)", + enablePeerAggregatedDiscovery: false, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;profile=nopeer", + wantGroupNames: []string{"local.example.com"}, + }, + { + name: "Request without profile (should default to peer-aggregated)", + enablePeerAggregatedDiscovery: true, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", + peerProvider: peerProvider, + wantGroupNames: []string{"local.example.com", "peer.example.com"}, + }, + { + name: "Request with unknown profile (should default to peer-aggregated)", + enablePeerAggregatedDiscovery: true, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;profile=foo", + peerProvider: peerProvider, + wantGroupNames: []string{"local.example.com", "peer.example.com"}, + }, + { + name: "Request with profile=nopeer (should get no-peer discovery)", + enablePeerAggregatedDiscovery: true, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;profile=nopeer", + peerProvider: peerProvider, + wantGroupNames: []string{"local.example.com"}, + }, + { + name: "Peer provider nil (should get local discovery)", + enablePeerAggregatedDiscovery: true, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", + peerProvider: nil, + wantGroupNames: []string{"local.example.com"}, + }, + { + name: "Multiple peer resources (should return all)", + enablePeerAggregatedDiscovery: true, + acceptHeader: "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", + peerProvider: &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer.example.com", "v2", "peer-resource"), + }, + "peer-server-2": { + newAPIGroup("peer2.example.com", "v1", "peer2-resource"), + }, + }, + }, + wantGroupNames: []string{ + "local.example.com", "peer.example.com", "peer2.example.com", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.UnknownVersionInteroperabilityProxy, tc.enablePeerAggregatedDiscovery) + + peerAggregatedDiscoveryManager := discoveryendpoint.NewPeerAggregatedDiscoveryHandler("test-server", manager, tc.peerProvider, "apis") + wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(manager, manager, peerAggregatedDiscoveryManager) + + resp, _, decoded := fetchPath(wrapped, tc.acceptHeader, "/apis", "") + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NotNil(t, decoded) + gotGroupNames := make([]string, 0, len(decoded.Items)) + for _, group := range decoded.Items { + gotGroupNames = append(gotGroupNames, group.Name) + } + assert.ElementsMatch(t, tc.wantGroupNames, gotGroupNames, "group names mismatch") + }) + } +} + +func TestPeerAggregatedDiscovery_ETagHandling(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.UnknownVersionInteroperabilityProxy, true) + manager := discoveryendpoint.NewResourceManager("apis") + localGroup := newAPIGroup("local.example.com", "v1", "local-resource") + manager.AddGroupVersion(localGroup.Name, localGroup.Versions[0]) + peerProvider := &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer.example.com", "v2", "peer-resource"), + }, + }, + } + + peerAggregatedDiscoveryManager := discoveryendpoint.NewPeerAggregatedDiscoveryHandler("test-server", manager, peerProvider, "apis") + wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(manager, manager, peerAggregatedDiscoveryManager) + + // First request, get ETag + rr1 := &responseRecorder{header: make(http.Header)} + req1, _ := http.NewRequest(http.MethodGet, "/apis", nil) + req1.Header.Set("Accept", "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList") + wrapped.ServeHTTP(rr1, req1) + etag1 := rr1.header.Get("ETag") + assert.NotEmpty(t, etag1, "ETag should be set on first response") + + // Second request, ETag should be the same (cache hit) + rr2 := &responseRecorder{header: make(http.Header)} + req2, _ := http.NewRequest(http.MethodGet, "/apis", nil) + req2.Header.Set("Accept", "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList") + wrapped.ServeHTTP(rr2, req2) + etag2 := rr2.header.Get("ETag") + assert.Equal(t, etag1, etag2, "ETag should be the same for cached response") + + // Invalidate cache and add a new peer resource + peerAggregatedDiscoveryManager.InvalidateCache() + gvr := schema.GroupVersionResource{Group: "peer.example.com", Version: "v2", Resource: "peer-resource2"} + peerProvider.addResource("peer-server-3", gvr, newAPIGroup(gvr.Group, gvr.Version, "peer-resource2")) + + // Third request, ETag should change + rr3 := &responseRecorder{header: make(http.Header)} + req3, _ := http.NewRequest(http.MethodGet, "/apis", nil) + req3.Header.Set("Accept", "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList") + wrapped.ServeHTTP(rr3, req3) + etag3 := rr3.header.Get("ETag") + assert.NotEmpty(t, etag3, "ETag should be set after resource change") + assert.NotEqual(t, etag1, etag3, "ETag should change when resources change") +} + +func TestFetchFromCache(t *testing.T) { + testCases := []struct { + name string + localGroups []apidiscoveryv2.APIGroupDiscovery + peerProvider discoveryendpoint.PeerDiscoveryProvider + wantGroups []string + }{ + { + name: "local only - no peer provider", + localGroups: []apidiscoveryv2.APIGroupDiscovery{ + newAPIGroup("local.example.com", "v1", "local-resource"), + }, + peerProvider: nil, + wantGroups: []string{"local.example.com"}, + }, + { + name: "local and peer resources", + localGroups: []apidiscoveryv2.APIGroupDiscovery{ + newAPIGroup("local.example.com", "v1", "local-resource"), + }, + peerProvider: &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer1.example.com", "v1", "peer1-resource"), + }, + "peer-server-2": { + newAPIGroup("peer2.example.com", "v1", "peer2-resource"), + }, + }, + }, + wantGroups: []string{"local.example.com", "peer1.example.com", "peer2.example.com"}, + }, + { + name: "no local resources, only peer", + localGroups: []apidiscoveryv2.APIGroupDiscovery{}, + peerProvider: &mockPeerDiscoveryProvider{ + resources: map[string][]apidiscoveryv2.APIGroupDiscovery{ + "peer-server-1": { + newAPIGroup("peer.example.com", "v1", "peer-resource"), + }, + }, + }, + wantGroups: []string{"peer.example.com"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + manager := discoveryendpoint.NewResourceManager("apis") + for _, group := range tc.localGroups { + for _, version := range group.Versions { + manager.AddGroupVersion(group.Name, version) + } + } + + response, etag := discoveryendpoint.TestFetchFromCache("test-server", manager, tc.peerProvider) + gotGroupNames := make([]string, 0, len(response.Items)) + for _, group := range response.Items { + gotGroupNames = append(gotGroupNames, group.Name) + } + + assert.ElementsMatch(t, tc.wantGroups, gotGroupNames, "group names mismatch") + assert.NotEmpty(t, etag, "ETag should be set") + }) + } +} + +func TestMergeResources(t *testing.T) { + r1 := apidiscoveryv2.APIResourceDiscovery{Resource: "r1"} + r2 := apidiscoveryv2.APIResourceDiscovery{Resource: "r2"} + + v1r1 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{r1}} + v1r2 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{r2}} + v2r1 := apidiscoveryv2.APIVersionDiscovery{Version: "v2", Resources: []apidiscoveryv2.APIResourceDiscovery{r1}} + + serverA := "serverA" + serverB := "serverB" + + // Local versions/resources + g1v1r1 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{v1r1}, + } + g2v1r1 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g2", UID: "g2-uid"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{v1r1}, + } + + // Peer versions/resources + g1v1r2 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-peer-uid"}, // Different UID + Versions: []apidiscoveryv2.APIVersionDiscovery{v1r2}, + } + g1v2r1 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-peer-uid"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{v2r1}, + } + + // g1v1r1 + g1v1r2 = g1v1(r1,r2) + g1v1r1r2Merged := apidiscoveryv2.APIGroupDiscovery{ + // Meta is copied from first server in the sorted list of serverIDs + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid"}, // Meta from g1v1r1 + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{r1, r2}}, + }, + } + + // g1v1r1 + g1v2r1 = g1(v1r1, v2r1) + g1v1r1v2r1Merged := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid"}, // Meta from g1v1r1 + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{r1}}, + {Version: "v2", Resources: []apidiscoveryv2.APIResourceDiscovery{r1}}, + }, + } + + // verbs test data + rVerbs1 := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Verbs: []string{"get", "list"}} + rVerbs2 := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Verbs: []string{"get", "watch"}} + rVerbsMerged := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Verbs: []string{"get", "list", "watch"}} // Verbs are sorted by helper + + vVerbs1 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rVerbs1}} + vVerbs2 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rVerbs2}} + vVerbsMerged := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rVerbsMerged}} + + gVerbs1 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-a"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{vVerbs1}, + } + gVerbs2 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-b"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{vVerbs2}, + } + gVerbsMerged := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-a"}, // Metadata from serverA (alphabetical first) + Versions: []apidiscoveryv2.APIVersionDiscovery{vVerbsMerged}, + } + + // subresource test data + rSub1 := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Subresources: []apidiscoveryv2.APISubresourceDiscovery{{Subresource: "status"}}} + rSub2 := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Subresources: []apidiscoveryv2.APISubresourceDiscovery{{Subresource: "log"}}} + rSubMerged := apidiscoveryv2.APIResourceDiscovery{Resource: "pods", Subresources: []apidiscoveryv2.APISubresourceDiscovery{{Subresource: "log"}, {Subresource: "status"}}} // Sorted by helper + + vSub1 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rSub1}} + vSub2 := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rSub2}} + vSubMerged := apidiscoveryv2.APIVersionDiscovery{Version: "v1", Resources: []apidiscoveryv2.APIResourceDiscovery{rSubMerged}} + + gSub1 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-a"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{vSub1}, + } + gSub2 := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-b"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{vSub2}, + } + gSubMerged := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: "g1", UID: "g1-uid-a"}, // Metadata from serverA + Versions: []apidiscoveryv2.APIVersionDiscovery{vSubMerged}, + } + + tests := []struct { + name string + localServerID string + localGroups []apidiscoveryv2.APIGroupDiscovery + peerGroupDiscovery map[string][]apidiscoveryv2.APIGroupDiscovery + wantResult []apidiscoveryv2.APIGroupDiscovery + validateShortCircuit bool + }{ + { + name: "no peers, just local resources", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, + validateShortCircuit: true, + }, + { + name: "peer adds no new g/v/r", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g1v1r1, g2v1r1}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, + validateShortCircuit: true, + }, + { + name: "peer adds a new resource to existing g/v", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g1v1r2}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1r2Merged}, + }, + { + name: "peer adds a new version to existing group", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g1v2r1}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1v2r1Merged}, + }, + { + name: "peer adds a completely new group", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g2v1r1}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, + }, + { + name: "peer has same data but different group order", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, // g1, g2 + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g2v1r1, g1v1r1}}, // g2, g1 + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, // Final order is [g1, g2] due to topo-sort + }, + { + name: "deterministic merge (Server A's view)", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g1v1r1}, // A has g1 + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverB": {g2v1r1}}, // B has g2 + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, // Sorted order is [g1, g2] + }, + { + name: "deterministic merge (Server B's view)", + localServerID: serverB, + localGroups: []apidiscoveryv2.APIGroupDiscovery{g2v1r1}, // B has g2 + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{"serverA": {g1v1r1}}, // A has g1 + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1, g2v1r1}, // Sorted order is [g1, g2] + }, + { + name: "peer adds a new verb to existing resource (triggers Case 2)", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{gVerbs1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{serverB: {gVerbs2}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{gVerbsMerged}, + }, + { + name: "peer adds a new subresource to existing resource (triggers Case 2)", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{gSub1}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{serverB: {gSub2}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{gSubMerged}, + }, + { + name: "empty local, non-empty peer (triggers Case 3)", + localServerID: serverA, + localGroups: []apidiscoveryv2.APIGroupDiscovery{}, + peerGroupDiscovery: map[string][]apidiscoveryv2.APIGroupDiscovery{serverB: {g1v1r1}}, + wantResult: []apidiscoveryv2.APIGroupDiscovery{g1v1r1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := discoveryendpoint.TestMergeResources(tt.localServerID, tt.localGroups, tt.peerGroupDiscovery) + if !reflect.DeepEqual(result, tt.wantResult) { + t.Errorf("mergeResources() mismatch (-want +got):\n%s", cmp.Diff(tt.wantResult, result)) + } + + // Test the short-circuit case specifically if requested + if tt.validateShortCircuit { + // reflect.ValueOf(slice).Pointer() gives the address of the underlying array. + // If the short-circuit worked, the result slice should be the *exact same* + // slice (same pointer) as localGroups, not just DeepEqual. + if reflect.ValueOf(result).Pointer() != reflect.ValueOf(tt.localGroups).Pointer() { + t.Errorf("Short-circuit failed: function returned a new slice instead of the original localGroups") + } + } + }) + } +} + +type mockPeerDiscoveryProvider struct { + resources map[string][]apidiscoveryv2.APIGroupDiscovery +} + +// responseRecorder is a minimal http.ResponseWriter for testing status codes and headers +type responseRecorder struct { + header http.Header + statusCode int + body strings.Builder +} + +func (r *responseRecorder) Header() http.Header { return r.header } +func (r *responseRecorder) Write(b []byte) (int, error) { return r.body.Write(b) } +func (r *responseRecorder) WriteHeader(statusCode int) { r.statusCode = statusCode } + +func (m *mockPeerDiscoveryProvider) GetPeerResources() map[string][]apidiscoveryv2.APIGroupDiscovery { + return m.resources +} + +func (m *mockPeerDiscoveryProvider) addResource(serverID string, gvr schema.GroupVersionResource, groupDiscovery apidiscoveryv2.APIGroupDiscovery) { + if m.resources == nil { + m.resources = make(map[string][]apidiscoveryv2.APIGroupDiscovery) + } + + if _, exists := m.resources[serverID]; !exists { + m.resources[serverID] = []apidiscoveryv2.APIGroupDiscovery{} + } + m.resources[serverID] = append(m.resources[serverID], groupDiscovery) +} + +func newAPIGroup(groupName, versionName, resourceName string) apidiscoveryv2.APIGroupDiscovery { + return apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: metav1.ObjectMeta{Name: groupName}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + { + Version: versionName, + Resources: []apidiscoveryv2.APIResourceDiscovery{ + {Resource: resourceName}, + }, + }, + }, + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper.go similarity index 66% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper.go index c9f0907e7..dc098c81b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper.go @@ -28,18 +28,33 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + genericfeatures "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) type WrappedHandler struct { - s runtime.NegotiatedSerializer - handler http.Handler - aggHandler http.Handler + s runtime.NegotiatedSerializer + handler http.Handler + aggHandler http.Handler + peerAggregatedHandler http.Handler } func (wrapped *WrappedHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - mediaType, _ := negotiation.NegotiateMediaTypeOptions(req.Header.Get("Accept"), wrapped.s.SupportedMediaTypes(), DiscoveryEndpointRestrictions) // mediaType.Convert looks at the request accept headers and is used to control whether the discovery document will be aggregated. + mediaType, _ := negotiation.NegotiateMediaTypeOptions(req.Header.Get("Accept"), wrapped.s.SupportedMediaTypes(), DiscoveryEndpointRestrictions) if IsAggregatedDiscoveryGVK(mediaType.Convert) { + if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.UnknownVersionInteroperabilityProxy) && mediaType.Convert.Version == "v2" { + if mediaType.Profile == "nopeer" { + NoPeerDiscoveryRequestCounter.Inc() + wrapped.aggHandler.ServeHTTP(resp, req) + return + } + if wrapped.peerAggregatedHandler != nil { + // Serve peer-aggregated discovery by default if provided. + wrapped.peerAggregatedHandler.ServeHTTP(resp, req) + return + } + } wrapped.aggHandler.ServeHTTP(resp, req) return } @@ -68,10 +83,15 @@ func (wrapped *WrappedHandler) GenerateWebService(prefix string, returnType inte // emit the aggregated discovery by passing in the aggregated // discovery type in content negotiation headers: eg: (Accept: // application/json;v=v2;g=apidiscovery.k8s.io;as=APIGroupDiscoveryList) -func WrapAggregatedDiscoveryToHandler(handler http.Handler, aggHandler http.Handler) *WrappedHandler { +// +// If peerAggregatedHandler is non-nil, it will be used to serve peer-aggregated +// discovery requests which aggregate discovery information from peer API servers. +// To request a no-peer aggregation, the client must include the "profile=nopeer" parameter in the +// Accept header eg: (Accept: application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;profile=nopeer). +func WrapAggregatedDiscoveryToHandler(handler, aggHandler, peerAggregatedHandler http.Handler) *WrappedHandler { scheme := runtime.NewScheme() utilruntime.Must(apidiscoveryv2.AddToScheme(scheme)) utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme)) codecs := serializer.NewCodecFactory(scheme) - return &WrappedHandler{codecs, handler, aggHandler} + return &WrappedHandler{codecs, handler, aggHandler, peerAggregatedHandler} } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper_test.go similarity index 63% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper_test.go index 5e25438c2..871ba565a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/aggregated/wrapper_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/aggregated/wrapper_test.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/endpoints/request" genericfeatures "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -40,6 +41,8 @@ const aggregatedV2Beta1JSONAccept = jsonAccept + aggregatedV2Beta1AcceptSuffix const aggregatedV2Beta1ProtoAccept = protobufAccept + aggregatedV2Beta1AcceptSuffix const aggregatedJSONAccept = jsonAccept + aggregatedAcceptSuffix const aggregatedProtoAccept = protobufAccept + aggregatedAcceptSuffix +const aggregatedNoPeerJSONAccept = jsonAccept + aggregatedAcceptSuffix + ";profile=nopeer" +const aggregatedNoPeerProtoAccept = protobufAccept + aggregatedAcceptSuffix + ";profile=nopeer" func fetchPath(handler http.Handler, path, accept string) string { w := httptest.NewRecorder() @@ -49,7 +52,7 @@ func fetchPath(handler http.Handler, path, accept string) string { req.Header.Set("Accept", accept) handler.ServeHTTP(w, req) - return string(w.Body.Bytes()) + return w.Body.String() } type fakeHTTPHandler struct { @@ -61,13 +64,16 @@ func (f fakeHTTPHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) } func TestAggregationEnabled(t *testing.T) { + featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.34")) unaggregated := fakeHTTPHandler{data: "unaggregated"} - aggregated := fakeHTTPHandler{data: "aggregated"} - wrapped := WrapAggregatedDiscoveryToHandler(unaggregated, aggregated) + aggregated := fakeHTTPHandler{data: "nopeer-aggregated"} + peerAggregated := fakeHTTPHandler{data: "peer-aggregated"} + wrapped := WrapAggregatedDiscoveryToHandler(unaggregated, aggregated, peerAggregated) testCases := []struct { - accept string - expected string + accept string + expected string + enablePeerAggregated bool }{ { // Misconstructed/incorrect accept headers should be passed to the unaggregated handler to return an error @@ -79,16 +85,16 @@ func TestAggregationEnabled(t *testing.T) { expected: "unaggregated", }, { accept: aggregatedV2Beta1JSONAccept, - expected: "aggregated", + expected: "nopeer-aggregated", }, { accept: aggregatedV2Beta1ProtoAccept, - expected: "aggregated", + expected: "nopeer-aggregated", }, { accept: aggregatedJSONAccept, - expected: "aggregated", + expected: "nopeer-aggregated", }, { accept: aggregatedProtoAccept, - expected: "aggregated", + expected: "nopeer-aggregated", }, { accept: jsonAccept, expected: "unaggregated", @@ -98,11 +104,38 @@ func TestAggregationEnabled(t *testing.T) { }, { // Server should return the first accepted type accept: aggregatedJSONAccept + "," + jsonAccept, - expected: "aggregated", + expected: "nopeer-aggregated", }, { // Server should return the first accepted type accept: aggregatedProtoAccept + "," + protobufAccept, - expected: "aggregated", + expected: "nopeer-aggregated", + }, + // Peer Agg discovery cases. + // profile is not set (should default to peer-aggregated) + { + accept: aggregatedJSONAccept, + expected: "peer-aggregated", + enablePeerAggregated: true, + }, { + accept: aggregatedProtoAccept, + expected: "peer-aggregated", + enablePeerAggregated: true, + }, + // profile=nopeer (should return no-peer) + { + accept: aggregatedNoPeerJSONAccept, + expected: "nopeer-aggregated", + enablePeerAggregated: true, + }, { + accept: aggregatedNoPeerProtoAccept, + expected: "nopeer-aggregated", + enablePeerAggregated: true, + }, + // profile is set to something other than no-peer (should default to peer-aggregated) + { + accept: aggregatedJSONAccept + ";profile=foo", + expected: "peer-aggregated", + enablePeerAggregated: true, }, } @@ -110,6 +143,9 @@ func TestAggregationEnabled(t *testing.T) { if tc.accept == aggregatedV2Beta1JSONAccept || tc.accept == aggregatedV2Beta1ProtoAccept { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryRemoveBetaType, false) } + if tc.enablePeerAggregated { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.UnknownVersionInteroperabilityProxy, true) + } body := fetchPath(wrapped, discoveryPath, tc.accept) assert.Equal(t, tc.expected, body) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/group.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/group.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/group.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/group.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/legacy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/legacy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/legacy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/legacy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/root.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/root.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/root.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/root.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/root_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/root_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/root_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/root_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/storageversionhash.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/storageversionhash.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/storageversionhash.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/storageversionhash.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/version.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/version.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/discovery/version.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/discovery/version.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filterlatency/filterlatency.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filterlatency/filterlatency.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filterlatency/filterlatency.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filterlatency/filterlatency.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filterlatency/filterlatency_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filterlatency/filterlatency_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filterlatency/filterlatency_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filterlatency/filterlatency_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit.go index d25bf35ae..83537bd21 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit.go @@ -149,7 +149,6 @@ func evaluatePolicyAndCreateAuditEvent(req *http.Request, policy audit.PolicyRul // writeLatencyToAnnotation writes the latency incurred in different // layers of the apiserver to the annotations of the audit object. -// it should be invoked after ev.StageTimestamp has been set appropriately. func writeLatencyToAnnotation(ctx context.Context) { ac := audit.AuditContextFrom(ctx) // we will track latency in annotation only when the total latency @@ -157,7 +156,7 @@ func writeLatencyToAnnotation(ctx context.Context) { // traces in rest/handlers for create, delete, update, // get, list, and deletecollection. const threshold = 500 * time.Millisecond - latency := ac.GetEventStageTimestamp().Sub(ac.GetEventRequestReceivedTimestamp().Time) + latency := time.Since(ac.GetEventRequestReceivedTimestamp().Time) if latency <= threshold { return } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_init.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_init.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_init.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_init.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_init_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_init_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_init_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_init_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_test.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_test.go index e2e77ac37..03bac7a07 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/audit_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/audit_test.go @@ -23,6 +23,7 @@ import ( "net/http/httptest" "net/url" "reflect" + "regexp" "sync" "testing" "time" @@ -224,7 +225,7 @@ func TestAudit(t *testing.T) { shortRunningPath := "/api/v1/namespaces/default/pods/foo" longRunningPath := "/api/v1/namespaces/default/pods?watch=true" - delay := 500 * time.Millisecond + delay := 501 * time.Millisecond for _, test := range []struct { desc string @@ -351,6 +352,10 @@ func TestAudit(t *testing.T) { Verb: "update", RequestURI: shortRunningPath, ResponseStatus: &metav1.Status{Code: 200}, + Annotations: map[string]string{ + "apiserver.latency.k8s.io/response-write": "^[0-9.]+[µnm]s$", + "apiserver.latency.k8s.io/total": "^[0-9.]+[µnm]s$", + }, }, }, true, @@ -713,6 +718,7 @@ func TestAudit(t *testing.T) { // simplified long-running check return ri.Verb == "watch" }) + handler = WithLatencyTrackers(handler) handler = WithAuditInit(handler) req, _ := http.NewRequestWithContext(ctx, test.verb, test.path, nil) @@ -772,6 +778,19 @@ func TestAudit(t *testing.T) { if (event.ResponseStatus != nil) && (event.ResponseStatus.Code != expect.ResponseStatus.Code) { t.Errorf("Unexpected status code : %d", event.ResponseStatus.Code) } + + for k, v := range expect.Annotations { + if actual, exists := event.Annotations[k]; !exists { + t.Errorf("Expect key %s in the annotations but it does not exist", k) + } else if matched, _ := regexp.MatchString(v, actual); !matched { + t.Errorf("Annotation %s value %q does not match regex %q", k, actual, v) + } + } + for k := range event.Annotations { + if _, exists := expect.Annotations[k]; !exists { + t.Errorf("Unexpected key %s in the annotations", k) + } + } } }) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authentication.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authentication.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authentication.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authentication.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authentication_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authentication_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authentication_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authentication_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authn_audit.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authn_audit.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authn_audit.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authn_audit.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authn_audit_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authn_audit_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authn_audit_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authn_audit_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authorization.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authorization.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authorization.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authorization.go index 6c6c94ba7..439696d07 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authorization.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authorization.go @@ -94,7 +94,7 @@ func withAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime. audit.AddAuditAnnotations(ctx, decisionAnnotationKey, decisionForbid, reasonAnnotationKey, reason) - responsewriters.Forbidden(ctx, attributes, w, req, reason, s) + responsewriters.Forbidden(attributes, w, req, reason, s) }) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authorization_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authorization_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/authorization_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/authorization_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/cachecontrol.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/cachecontrol.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/cachecontrol.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/cachecontrol.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/cachecontrol_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/cachecontrol_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/cachecontrol_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/cachecontrol_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/doc.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/cache.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/cache.go new file mode 100644 index 000000000..c0ef42c58 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/cache.go @@ -0,0 +1,332 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package impersonation + +import ( + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "hash/fnv" + "time" + "unsafe" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/cache" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/utils/lru" +) + +// modeIndexCache is a simple username -> impersonation mode cache that is based on the assumption +// that a particular user is likely to use a single mode of impersonation for all impersonated requests +// that they make. it remembers which impersonation mode was last successful for a username, and tries +// that mode first for future impersonation checks. this makes it so that the amortized cost of legacy +// impersonation remains the same, and the cost of constrained impersonation is one extra authorization +// check in additional to the existing checks of regular impersonation. +type modeIndexCache struct { + cache *lru.Cache +} + +func (c *modeIndexCache) get(attributes authorizer.Attributes) (int, bool) { + idx, ok := c.cache.Get(modeIndexCacheKey(attributes)) + if !ok { + return 0, false + } + return idx.(int), true +} + +func (c *modeIndexCache) set(attributes authorizer.Attributes, idx int) { + c.cache.Add(modeIndexCacheKey(attributes), idx) +} + +func modeIndexCacheKey(attributes authorizer.Attributes) string { + key := attributes.GetUser().GetName() + // hash the name so our cache size is predicable regardless of the size of usernames + // collisions do not matter for this logic as it simply changes the ordering of the modes used + h := fnv.New128a() + h.Write([]byte(key)) + var sum [16]byte + h.Sum(sum[:0]) + buf := make([]byte, 32) // 16 bytes of hash -> 32 hex chars + hex.Encode(buf, sum[:]) + return toString(buf) // only safe because buf is heap allocated via make +} + +func newModeIndexCache() *modeIndexCache { + return &modeIndexCache{ + // each entry is roughly ~100 bytes (hashed string key + int value + LRU list/map overhead) + // thus at even 10k entries, we should use about 1 MB memory + // this hardcoded size allows us to remember many users without leaking memory + cache: lru.New(10_000), + } +} + +// impersonationCache tracks successful impersonation attempts for a given mode with a short TTL. +// +// when skipAttributes is false, it maps [wantedUser, attributes] -> impersonatedUserInfo +// when skipAttributes is true, it maps [wantedUser, requestor] -> impersonatedUserInfo +// +// thus each constrained impersonation mode needs two of these caches: +// the outer cache sets skipAttributes to false and thus covers the overall impersonation attempt, see constrainedImpersonationModeState.check. +// the inner cache sets skipAttributes to true and only covers the authorization checks that +// are not dependent on the specific request being made, see impersonationModeState.check. +type impersonationCache struct { + cache *cache.Expiring + skipAttributes bool +} + +func (c *impersonationCache) get(k *impersonationCacheKey) *impersonatedUserInfo { + key, err := k.key(c.skipAttributes) + if err != nil { + utilruntime.HandleError(fmt.Errorf("failed to build impersonation cache key: %w", err)) + return nil + } + impersonatedUser, ok := c.cache.Get(key) + if !ok { + return nil + } + return impersonatedUser.(*impersonatedUserInfo) +} + +func (c *impersonationCache) set(k *impersonationCacheKey, impersonatedUser *impersonatedUserInfo) { + key, err := k.key(c.skipAttributes) + if err != nil { + utilruntime.HandleError(fmt.Errorf("failed to build impersonation cache key: %w", err)) + return + } + c.cache.Set(key, impersonatedUser, 10*time.Second) // hardcode the same short TTL as used by TokenSuccessCacheTTL +} + +func newImpersonationCache(skipAttributes bool) *impersonationCache { + return &impersonationCache{ + cache: cache.NewExpiring(), + skipAttributes: skipAttributes, + } +} + +// The attribute accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ authorizer.Attributes = (interface { + GetUser() user.Info + GetVerb() string + IsReadOnly() bool + GetNamespace() string + GetResource() string + GetSubresource() string + GetName() string + GetAPIGroup() string + GetAPIVersion() string + IsResourceRequest() bool + GetPath() string + GetFieldSelector() (fields.Requirements, error) + GetLabelSelector() (labels.Requirements, error) +})(nil) + +// The user info accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ user.Info = (interface { + GetName() string + GetUID() string + GetGroups() []string + GetExtra() map[string][]string +})(nil) + +// impersonationCacheKey allows for lazy building of string cache keys based on the inputs. +// See impersonationCache details above for the semantics around skipAttributes. +// Note that the same impersonationCacheKey can be used with any value for skipAttributes. +type impersonationCacheKey struct { + wantedUser *user.DefaultInfo + attributes authorizer.Attributes + + // lazily calculated values at point of use + keyAttr string + errAttr error + keyUser string + errUser error +} + +func (k *impersonationCacheKey) key(skipAttributes bool) (string, error) { + if skipAttributes { + return k.keyWithoutAttributes() + } + return k.keyWithAttributes() +} + +func (k *impersonationCacheKey) keyWithAttributes() (string, error) { + if len(k.keyAttr) != 0 || k.errAttr != nil { + return k.keyAttr, k.errAttr + } + + k.keyAttr, k.errAttr = buildKey(k.wantedUser, k.attributes) + return k.keyAttr, k.errAttr +} + +func (k *impersonationCacheKey) keyWithoutAttributes() (string, error) { + if len(k.keyUser) != 0 || k.errUser != nil { + return k.keyUser, k.errUser + } + + // fake attributes that just contain the requestor to allow us to reuse buildKey + requestor := k.attributes.GetUser() + attributes := authorizer.AttributesRecord{User: requestor} + + k.keyUser, k.errUser = buildKey(k.wantedUser, attributes) + return k.keyUser, k.errUser +} + +// buildKey creates a hashed string key based on the inputs that is namespaced to the requestor. +// A cryptographically secure hash is used to minimize the chance of collisions. +func buildKey(wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (string, error) { + fieldSelector, err := attributes.GetFieldSelector() + if err != nil { + return "", err // if we do not fully understand the attributes, just skip caching altogether + } + + labelSelector, err := attributes.GetLabelSelector() + if err != nil { + return "", err // if we do not fully understand the attributes, just skip caching altogether + } + + requestor := attributes.GetUser() + + // the chance of a hash collision is impractically small, but the only way that would lead to a + // privilege escalation is if you could get the cache key of a different user. if you somehow + // get a collision with your own username, you already have that permission since we only set + // values in the cache after a successful impersonation. Thus, we include the requestor + // username in the cache key. It is safe to assume that a user has no control over their own + // username since that is controlled by the authenticator. Even though many of the other inputs + // are under the control of the requestor, they cannot explode the cache due to the hashing. + b := newCacheKeyBuilder(requestor.GetName()) + + addUser(b, wantedUser) + addUser(b, requestor) + + b.addLengthPrefixed(func(b *cacheKeyBuilder) { + b. + addString(attributes.GetVerb()). + addBool(attributes.IsReadOnly()). + addString(attributes.GetNamespace()). + addString(attributes.GetResource()). + addString(attributes.GetSubresource()). + addString(attributes.GetName()). + addString(attributes.GetAPIGroup()). + addString(attributes.GetAPIVersion()). + addBool(attributes.IsResourceRequest()). + addString(attributes.GetPath()) + }) + + b.addLengthPrefixed(func(b *cacheKeyBuilder) { + for _, req := range fieldSelector { + b.addStringSlice([]string{req.Field, string(req.Operator), req.Value}) + } + }) + + b.addLengthPrefixed(func(b *cacheKeyBuilder) { + for _, req := range labelSelector { + b.addString(req.String()) + } + }) + + return b.build(), nil +} + +func addUser(b *cacheKeyBuilder, u user.Info) { + b.addLengthPrefixed(func(b *cacheKeyBuilder) { + b. + addString(u.GetName()). + addString(u.GetUID()). + addStringSlice(u.GetGroups()). + addLengthPrefixed(func(b *cacheKeyBuilder) { + extra := u.GetExtra() + for _, key := range sets.StringKeySet(extra).List() { + b.addString(key) + b.addStringSlice(extra[key]) + } + }) + }) +} + +// cacheKeyBuilder builds a binary key from structured inputs using uint32 length-prefixed encoding, then hashes +// it with SHA-256. All methods operate on a single shared buffer to avoid closure and child-builder allocations. +type cacheKeyBuilder struct { + namespace string // in the programming sense, not the Kubernetes concept + builder []byte +} + +func newCacheKeyBuilder(namespace string) *cacheKeyBuilder { + // start with a reasonable size to avoid too many allocations + return &cacheKeyBuilder{namespace: namespace, builder: make([]byte, 0, 384)} +} + +func (c *cacheKeyBuilder) addString(value string) *cacheKeyBuilder { + c.addLengthPrefixed(func(c *cacheKeyBuilder) { + c.builder = append(c.builder, value...) + }) + return c +} + +func (c *cacheKeyBuilder) addStringSlice(values []string) *cacheKeyBuilder { + c.addLengthPrefixed(func(c *cacheKeyBuilder) { + for _, v := range values { + c.addString(v) + } + }) + return c +} + +func (c *cacheKeyBuilder) addBool(value bool) *cacheKeyBuilder { + var b byte + if value { + b = 1 + } + c.builder = append(c.builder, b) + return c +} + +type builderContinuation func(child *cacheKeyBuilder) + +func (c *cacheKeyBuilder) addLengthPrefixed(f builderContinuation) { + offset := len(c.builder) + c.builder = append(c.builder, 0, 0, 0, 0) // placeholder for uint32 length + f(c) + binary.BigEndian.PutUint32(c.builder[offset:], uint32(len(c.builder)-offset-4)) +} + +func (c *cacheKeyBuilder) build() string { + hashed := sha256.Sum256(c.builder) // reduce the size of the cache key to keep the overall cache size small + // sha256 = 32 bytes -> 64 hex chars + "/" + namespace + buf := make([]byte, 0, 64+1+len(c.namespace)) + buf = hex.AppendEncode(buf, hashed[:]) + buf = append(buf, '/') + buf = append(buf, c.namespace...) + return toString(buf) // only safe because buf is heap allocated via make +} + +// toString performs unholy acts to avoid allocations +func toString(b []byte) string { + // unsafe.SliceData relies on cap whereas we want to rely on len + if len(b) == 0 { + return "" + } + // Copied from go 1.20.1 strings.Builder.String + // https://github.com/golang/go/blob/202a1a57064127c3f19d96df57b9f9586145e21c/src/strings/builder.go#L48 + return unsafe.String(unsafe.SliceData(b), len(b)) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation.go new file mode 100644 index 000000000..e2a6e953d --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation.go @@ -0,0 +1,322 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package impersonation + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + authenticationv1 "k8s.io/api/authentication/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/filters" + "k8s.io/apiserver/pkg/endpoints/filters/impersonation/metrics" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/server/httplog" + "k8s.io/klog/v2" +) + +// WithConstrainedImpersonation implements constrained impersonation as described in https://kep.k8s.io/5284 +// It also includes a complete reimplementation of legacy impersonation for backwards compatibility. +// At a high level, constrained impersonation uses multiple authorization checks to allow for the granular +// expression of impersonation access. For example, a service account may be authorized to impersonate the +// node that it is associated with but only when listing pods. See the linked KEP for further details. +func WithConstrainedImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler { + metrics.RegisterMetrics() + + ma := &metricsAuthorizer{ + delegate: a, + recordAuthorizationCall: metrics.RecordImpersonationAuthorizationCall, + } + + recordAttempt := func(ctx context.Context, mode, decision string, duration time.Duration) { + metrics.RecordImpersonationAttempt(mode, decision, duration) + request.TrackImpersonationLatency(ctx, duration) + } + + return &constrainedImpersonationHandler{ + handler: handler, + tracker: newImpersonationModesTracker(ma), + s: s, + + recordAttempt: recordAttempt, + metricsAuthorizer: ma, + } +} + +type constrainedImpersonationHandler struct { + handler http.Handler + tracker *impersonationModesTracker + s runtime.NegotiatedSerializer + + // to allow unit tests to override metrics recording + recordAttempt func(ctx context.Context, mode, decision string, duration time.Duration) + metricsAuthorizer *metricsAuthorizer +} + +func (c *constrainedImpersonationHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + + wantedUser, err := processImpersonationHeaders(req.Header) + if err != nil { + responsewriters.RespondWithError(w, req, err, c.s) + return + } + if wantedUser == nil { // impersonation was not attempted so skip to the next handler + c.handler.ServeHTTP(w, req) + return + } + attributes, err := filters.GetAuthorizerAttributes(ctx) + if err != nil { + responsewriters.InternalError(w, req, err) + return + } + requestor := attributes.GetUser() + if requestor == nil { + responsewriters.InternalError(w, req, errors.New("no User found in the context")) + return + } + + start := time.Now() + impersonatedUser, err := c.tracker.getImpersonatedUser(ctx, wantedUser, attributes) + duration := time.Since(start) + if err != nil { + c.recordAttempt(ctx, "", "denied", duration) + klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "err", err) + responsewriters.RespondWithError(w, req, err, c.s) + return + } + c.recordAttempt(ctx, modeFromConstraint(impersonatedUser.constraint), "allowed", duration) + + req = req.WithContext(request.WithUser(ctx, impersonatedUser.user)) + httplog.LogOf(req, w).Addf("%v is impersonating %v", userString(requestor), userString(impersonatedUser.user)) + audit.LogImpersonatedUser(ctx, impersonatedUser.user, impersonatedUser.constraint) + + c.handler.ServeHTTP(w, req) +} + +// processImpersonationHeaders converts the impersonation headers in the given input headers +// into the equivalent user.DefaultInfo. The resulting user is a raw representation of the +// input headers, that is, no defaulting or other mutations have been applied to it. +func processImpersonationHeaders(headers http.Header) (*user.DefaultInfo, error) { + wantedUser := &user.DefaultInfo{} + + wantedUser.Name = headers.Get(authenticationv1.ImpersonateUserHeader) + hasUser := len(wantedUser.Name) > 0 + + wantedUser.UID = headers.Get(authenticationv1.ImpersonateUIDHeader) + hasUID := len(wantedUser.UID) > 0 + + hasGroups := false + for _, group := range headers[authenticationv1.ImpersonateGroupHeader] { + hasGroups = true + wantedUser.Groups = append(wantedUser.Groups, group) + } + + hasUserExtra := false + for headerName, values := range headers { + if !strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) { + continue + } + + hasUserExtra = true + + if len(values) == 0 { + // this looks a little strange but matches the behavior of buildImpersonationRequests from legacy impersonation + // http1 uses textproto.Reader#ReadMIMEHeader which does seem to allow an empty slice for values + // http2 uses http.Header#Add which will cause the values slice to always be non-empty + continue + } + + extraKey := unescapeExtraKey(strings.ToLower(headerName[len(authenticationv1.ImpersonateUserExtraHeaderPrefix):])) + + if wantedUser.Extra == nil { + wantedUser.Extra = map[string][]string{} + } + wantedUser.Extra[extraKey] = append(wantedUser.Extra[extraKey], values...) + } + + if !hasUser && (hasUID || hasGroups || hasUserExtra) { + return nil, apierrors.NewBadRequest(fmt.Sprintf("requested %#v without impersonating a user name", wantedUser)) + } + + if !hasUser { + return nil, nil + } + + // clear all the impersonation headers from the request to prevent downstream layers from knowing that impersonation was used + // we do not want anything outside of this package trying to behave differently based on if impersonation was used + headers.Del(authenticationv1.ImpersonateUserHeader) + headers.Del(authenticationv1.ImpersonateUIDHeader) + headers.Del(authenticationv1.ImpersonateGroupHeader) + for headerName := range headers { + if strings.HasPrefix(headerName, authenticationv1.ImpersonateUserExtraHeaderPrefix) { + headers.Del(headerName) + } + } + + return wantedUser, nil +} + +// impersonationModesTracker records which impersonation mode was last successful for a given requestor user. +// this allows us to check for the more secure constrained impersonation modes first while keeping the overall +// cost of legacy impersonation unchanged (as we will support legacy impersonation forever). +type impersonationModesTracker struct { + modes []impersonationMode + idxCache *modeIndexCache +} + +func newImpersonationModesTracker(a authorizer.Authorizer) *impersonationModesTracker { + loggingAuthorizer := authorizer.AuthorizerFunc(func(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { + decision, reason, err := a.Authorize(ctx, attributes) + // build a detailed log of the authorization + // make the whole block conditional so we do not do a lot of string-building we will not use + if klogV := klog.V(5); klogV.Enabled() { // same log level that the RBAC authorizer uses for verbose logging + u := attributes.GetUser() + fieldSelector, _ := attributes.GetFieldSelector() + labelSelector, _ := attributes.GetLabelSelector() + klogV.InfoSDepth(3, "Impersonation authorization check", + // we cannot just pass attributes to the logger as that will not capture the actual result of calling these methods + // impersonation makes heavy use of wrapping these methods to add extra logic + "username", u.GetName(), + "uid", u.GetUID(), + "groups", u.GetGroups(), + "extra", u.GetExtra(), + + "isResourceRequest", attributes.IsResourceRequest(), + + "namespace", attributes.GetNamespace(), + "verb", attributes.GetVerb(), + "group", attributes.GetAPIGroup(), + "version", attributes.GetAPIVersion(), + "resource", attributes.GetResource(), + "subresource", attributes.GetSubresource(), + "name", attributes.GetName(), + "fieldSelector", fieldSelector, + "labelSelector", labelSelector, + + "path", attributes.GetPath(), + + "decision", decision, + "reason", reason, + "err", err, + ) + } + return decision, reason, err + }) + return &impersonationModesTracker{ + modes: allImpersonationModes(loggingAuthorizer), + idxCache: newModeIndexCache(), + } +} + +func (t *impersonationModesTracker) getImpersonatedUser(ctx context.Context, wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (*impersonatedUserInfo, error) { + // share a single cache key across all modes so that we only lazily build it once + key := &impersonationCacheKey{wantedUser: wantedUser, attributes: attributes} + var firstErr error + + // try the last successful mode first to reduce the amortized cost of impersonation + // we attempt all modes unless we short-circuit due to a successful impersonation + modeIdx, modeIdxOk := t.idxCache.get(attributes) + if modeIdxOk { + impersonatedUser, err := t.modes[modeIdx].check(ctx, key, wantedUser, attributes) + if err == nil && impersonatedUser != nil { + return impersonatedUser, nil + } + firstErr = err + } + + for i, mode := range t.modes { + if modeIdxOk && i == modeIdx { + continue // skip already attempted mode + } + + impersonatedUser, err := mode.check(ctx, key, wantedUser, attributes) + if err != nil { + if firstErr == nil { + firstErr = err + } + continue + } + if impersonatedUser == nil { + continue + } + t.idxCache.set(attributes, i) + return impersonatedUser, nil + } + + if firstErr != nil { + return nil, firstErr + } + + // this should not happen, but make sure we fail closed when no impersonation mode succeeded + return nil, errors.New("all impersonation modes failed") +} + +type metricsAuthorizer struct { + delegate authorizer.Authorizer + recordAuthorizationCall func(mode, decision string, duration time.Duration) +} + +func (m *metricsAuthorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { + start := time.Now() + decision, reason, err := m.delegate.Authorize(ctx, attributes) + duration := time.Since(start) + + m.recordAuthorizationCall(modeFromVerb(attributes.GetVerb()), decisionToLabel(decision), duration) + + return decision, reason, err +} + +func modeFromConstraint(constraint string) string { + if len(constraint) == 0 { + return "legacy" + } + return modeFromVerb(constraint) +} + +func modeFromVerb(verb string) string { + switch { + case verb == "impersonate": + return "legacy" + case verb == "impersonate:associated-node" || strings.HasPrefix(verb, "impersonate-on:associated-node:"): + return "associated-node" + case verb == "impersonate:arbitrary-node" || strings.HasPrefix(verb, "impersonate-on:arbitrary-node:"): + return "arbitrary-node" + case verb == "impersonate:serviceaccount" || strings.HasPrefix(verb, "impersonate-on:serviceaccount:"): + return "serviceaccount" + case verb == "impersonate:user-info" || strings.HasPrefix(verb, "impersonate-on:user-info:"): + return "user-info" + default: + return "unknown" + } +} + +func decisionToLabel(decision authorizer.Decision) string { + if decision == authorizer.DecisionAllow { + return "allowed" + } + return "denied" +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go new file mode 100644 index 000000000..80e771dae --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/constrained_impersonation_test.go @@ -0,0 +1,2406 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package impersonation + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "hash/fnv" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit/policy" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/filters" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/transport" +) + +type constrainedImpersonationTest struct { + t testing.TB + + constrainedImpersonationHandler *constrainedImpersonationHandler + authzChecks []authzCheck + echoCalled bool + auditEvents []*auditinternal.Event + + attemptMode string + attemptDecision string + authorizationMetrics map[string]int // "mode/decision" -> count + + // only used for parallelHandler + lock sync.Mutex +} + +type authzCheck struct { + attributes authorizer.AttributesRecord + decision authorizer.Decision +} + +func (c *constrainedImpersonationTest) ProcessEvents(evs ...*auditinternal.Event) bool { + for _, e := range evs { + c.auditEvents = append(c.auditEvents, e.DeepCopy()) // event is mutated in place so capture its current state + } + return true +} + +func (c *constrainedImpersonationTest) Authorize(ctx context.Context, a authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { + u := a.GetUser() + + defer func() { + c.lock.Lock() + c.authzChecks = append(c.authzChecks, authzCheck{ + attributes: comparableAttributes(a), + decision: decision, + }) + c.lock.Unlock() + }() + + if u.GetName() == "sa-impersonater" && a.GetVerb() == "impersonate:serviceaccount" && a.GetResource() == "serviceaccounts" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() == "system:serviceaccount:default:node" && a.GetVerb() == "impersonate:arbitrary-node" && a.GetResource() == "nodes" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() == "node-impersonater" && a.GetVerb() == "impersonate:arbitrary-node" && a.GetResource() == "nodes" { + return authorizer.DecisionAllow, "", nil + } + + if len(u.GetGroups()) > 0 && u.GetGroups()[0] == "associate-node-impersonater" && a.GetVerb() == "impersonate:associated-node" && a.GetResource() == "nodes" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() == "user-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() == "user-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "uids" { + return authorizer.DecisionAllow, "", nil + } + + if len(u.GetGroups()) > 0 && u.GetGroups()[0] == "group-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" { + return authorizer.DecisionAllow, "", nil + } + + if len(u.GetGroups()) > 0 && u.GetGroups()[0] == "extra-setter-scopes" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "userextras" && a.GetSubresource() == "pandas.io/scopes" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() == "legacy-impersonater" && a.GetVerb() == "impersonate" { + return authorizer.DecisionAllow, "", nil + } + + if u.GetName() != "legacy-impersonator" && + strings.HasPrefix(a.GetVerb(), "impersonate-on:") && + (strings.HasSuffix(a.GetVerb(), "list") || strings.HasSuffix(a.GetVerb(), "get")) { + return authorizer.DecisionAllow, "", nil + } + + // many-groups-impersonater: can impersonate users and any groups via wildcard + if u.GetName() == "many-groups-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + if u.GetName() == "many-groups-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" && a.GetName() == "*" { + return authorizer.DecisionAllow, "", nil + } + + // many-extras-impersonater: can impersonate users and any extras via wildcard + if u.GetName() == "many-extras-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + if u.GetName() == "many-extras-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "userextras" && a.GetSubresource() == "*" && a.GetName() == "*" { + return authorizer.DecisionAllow, "", nil + } + + // mixed-impersonater: can impersonate users, specific groups, and specific extras (but NOT via wildcards) + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "users" { + return authorizer.DecisionAllow, "", nil + } + // Allow specific group names, but NOT wildcard + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "groups" && a.GetName() != "*" { + return authorizer.DecisionAllow, "", nil + } + // Allow specific extras for tags.io/env, but NOT wildcard + if u.GetName() == "mixed-impersonater" && a.GetVerb() == "impersonate:user-info" && a.GetResource() == "userextras" && + a.GetSubresource() == "tags.io/env" && a.GetName() != "*" { + return authorizer.DecisionAllow, "", nil + } + + return authorizer.DecisionNoOpinion, "deny by default", nil +} + +func (c *constrainedImpersonationTest) echoUserInfoHandler() http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + c.lock.Lock() + c.echoCalled = true + c.lock.Unlock() + + u, ok := request.UserFrom(req.Context()) + if !ok { + c.t.Fatal("user not found in request") + } + + _ = json.NewEncoder(w).Encode(comparableUser(u)) + + if _, ok := req.Header[authenticationv1.ImpersonateUserHeader]; ok { + c.t.Fatal("user header still present") + } + if _, ok := req.Header[authenticationv1.ImpersonateUIDHeader]; ok { + c.t.Fatal("uid header still present") + } + if _, ok := req.Header[authenticationv1.ImpersonateGroupHeader]; ok { + c.t.Fatal("group header still present") + } + for key := range req.Header { + if strings.HasPrefix(key, authenticationv1.ImpersonateUserExtraHeaderPrefix) { + c.t.Fatalf("extra header still present: %v", key) + } + } + } +} + +func (c *constrainedImpersonationTest) authenticationHandler(handler http.Handler) http.Handler { + return filters.WithAuthentication(handler, authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { + userData := req.Header.Get(testUserHeader) + if len(userData) == 0 { + c.t.Fatal("missing user header") + } + var u user.DefaultInfo + if err := json.Unmarshal([]byte(userData), &u); err != nil { + c.t.Fatal(err) + } + return &authenticator.Response{User: &u}, true, nil + }), nil, nil, nil) +} + +func (c *constrainedImpersonationTest) requestInfoHandler(handler http.Handler) http.Handler { + return filters.WithRequestInfo(handler, requestInfoFunc(func(req *http.Request) (*request.RequestInfo, error) { + requestInfoData := req.Header.Get(testRequestInfoHeader) + if len(requestInfoData) == 0 { + c.t.Fatal("missing request info header") + } + var r request.RequestInfo + if err := json.Unmarshal([]byte(requestInfoData), &r); err != nil { + c.t.Fatal(err) + } + return &r, nil + })) +} + +type requestInfoFunc func(*http.Request) (*request.RequestInfo, error) + +func (f requestInfoFunc) NewRequestInfo(req *http.Request) (*request.RequestInfo, error) { + return f(req) +} + +const ( + testUserHeader = "insecure-test-user-json" + testRequestInfoHeader = "insecure-test-request-info-json" +) + +func (c *constrainedImpersonationTest) handler() http.Handler { + s := runtime.NewScheme() + metav1.AddToGroupVersion(s, metav1.SchemeGroupVersion) + addImpersonation := WithConstrainedImpersonation(c.echoUserInfoHandler(), c, serializer.NewCodecFactory(s)) + c.constrainedImpersonationHandler = addImpersonation.(*constrainedImpersonationHandler) + + recordAttempt := c.constrainedImpersonationHandler.recordAttempt + c.constrainedImpersonationHandler.recordAttempt = func(ctx context.Context, mode, decision string, duration time.Duration) { + c.attemptMode = mode + c.attemptDecision = decision + recordAttempt(ctx, mode, decision, duration) + } + c.constrainedImpersonationHandler.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) { + if c.authorizationMetrics == nil { + c.authorizationMetrics = map[string]int{} + } + c.authorizationMetrics[mode+"/"+decision]++ + } + + fakeRuleEvaluator := policy.NewFakePolicyRuleEvaluator(auditinternal.LevelRequestResponse, nil) + + // follow the same handler chain order as DefaultBuildHandlerChain + addAudit := filters.WithAudit(addImpersonation, c, fakeRuleEvaluator, nil) + addAuthentication := c.authenticationHandler(addAudit) + addLatencyTrackers := filters.WithLatencyTrackers(addAuthentication) + addRequestInfo := c.requestInfoHandler(addLatencyTrackers) + // set received timestamp >500ms in the past so audit annotations are emitted + addReceivedTimestamp := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + req = req.WithContext(request.WithReceivedTimestamp(req.Context(), time.Now().Add(-time.Hour))) + addRequestInfo.ServeHTTP(w, req) + }) + addAuditInit := filters.WithAuditInit(addReceivedTimestamp) + return addAuditInit +} + +func (c *constrainedImpersonationTest) parallelHandler() http.Handler { + s := runtime.NewScheme() + metav1.AddToGroupVersion(s, metav1.SchemeGroupVersion) + addImpersonation := WithConstrainedImpersonation(c.echoUserInfoHandler(), c, serializer.NewCodecFactory(s)) + c.constrainedImpersonationHandler = addImpersonation.(*constrainedImpersonationHandler) + + c.constrainedImpersonationHandler.recordAttempt = func(ctx context.Context, mode, decision string, _ time.Duration) {} + c.constrainedImpersonationHandler.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) {} + + addAuthentication := c.authenticationHandler(addImpersonation) + addRequestInfo := c.requestInfoHandler(addAuthentication) + return addRequestInfo +} + +type testRoundTripper struct { + user *user.DefaultInfo + requestInfo *request.RequestInfo + delegate http.RoundTripper +} + +func (t *testRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + userData, err := json.Marshal(t.user) + if err != nil { + return nil, err + } + requestInfoData, err := json.Marshal(t.requestInfo) + if err != nil { + return nil, err + } + r.Header.Set(testUserHeader, string(userData)) + r.Header.Set(testRequestInfoHeader, string(requestInfoData)) + return t.delegate.RoundTrip(r) +} + +func (c *constrainedImpersonationTest) assertAttributes(r testRequest) { + authzChecks := c.authzChecks + c.authzChecks = nil + + require.Len(c.t, authzChecks, len(r.expectedAuthzChecks)) + + // normally all authorization checks are done against the requestor + // but in some cases such as associated-node, we check against a slightly different user + expectedAttributesUser := r.expectedAttributesUser + if expectedAttributesUser == nil { + expectedAttributesUser = r.requestor + } + + for i := range len(r.expectedAuthzChecks) { + expectedAttributes := withUser(r.expectedAuthzChecks[i].attributes, expectedAttributesUser) + require.Equal(c.t, expectedAttributes, authzChecks[i].attributes, "authz check %d: attributes mismatch", i) + require.Equal(c.t, r.expectedAuthzChecks[i].decision, authzChecks[i].decision, "authz check %d: decision mismatch", i) + } +} + +func comparableAttributes(attributes authorizer.Attributes) authorizer.AttributesRecord { + fs, errFS := attributes.GetFieldSelector() + ls, errLS := attributes.GetLabelSelector() + return authorizer.AttributesRecord{ + User: comparableUser(attributes.GetUser()), + Verb: attributes.GetVerb(), + Namespace: attributes.GetNamespace(), + APIGroup: attributes.GetAPIGroup(), + APIVersion: attributes.GetAPIVersion(), + Resource: attributes.GetResource(), + Subresource: attributes.GetSubresource(), + Name: attributes.GetName(), + ResourceRequest: attributes.IsResourceRequest(), + Path: attributes.GetPath(), + FieldSelectorRequirements: fs, + FieldSelectorParsingErr: errFS, + LabelSelectorRequirements: ls, + LabelSelectorParsingErr: errLS, + } +} + +func comparableAuditUser(u *authenticationv1.UserInfo) *user.DefaultInfo { + if u == nil { + return nil + } + var extra map[string][]string + if len(u.Extra) > 0 { + extra = make(map[string][]string, len(u.Extra)) + for k, v := range u.Extra { + extra[k] = v + } + } + return &user.DefaultInfo{ + Name: u.Username, + UID: u.UID, + Groups: u.Groups, + Extra: extra, + } +} + +func comparableUser(u user.Info) *user.DefaultInfo { + return &user.DefaultInfo{ + Name: u.GetName(), + UID: u.GetUID(), + Groups: u.GetGroups(), + Extra: u.GetExtra(), + } +} + +func (c *constrainedImpersonationTest) assertEchoCalled(expectedCalled bool) { + called := c.echoCalled + c.echoCalled = false + require.Equal(c.t, expectedCalled, called) +} + +func (c *constrainedImpersonationTest) assertMetrics(r testRequest) { + c.t.Helper() + + attemptMode := c.attemptMode + attemptDecision := c.attemptDecision + authorizationMetrics := c.authorizationMetrics + c.attemptMode = "" + c.attemptDecision = "" + c.authorizationMetrics = nil + + require.Equal(c.t, r.expectedAttemptMode, attemptMode, "unexpected attempt mode") + require.Equal(c.t, r.expectedAttemptDecision, attemptDecision, "unexpected attempt decision") + require.Equal(c.t, r.expectedAuthorizationMetrics, authorizationMetrics, "unexpected authorization metrics") +} + +func (c *constrainedImpersonationTest) assertAuditEvents(r testRequest) { + c.t.Helper() + + events := c.auditEvents + c.auditEvents = nil + + require.Len(c.t, events, 2) + + requestReceived := events[0] + responseComplete := events[1] + + require.Empty(c.t, requestReceived.Annotations["apiserver.latency.k8s.io/impersonation"]) + require.Regexp(c.t, "^[0-9.]+[µnm]s$", responseComplete.Annotations["apiserver.latency.k8s.io/impersonation"]) + + require.Nil(c.t, requestReceived.ImpersonatedUser) + require.Equal(c.t, r.expectedImpersonatedUser, comparableAuditUser(responseComplete.ImpersonatedUser)) + + require.Nil(c.t, requestReceived.ResponseStatus) + require.Equal(c.t, r.expectedMessage, responseComplete.ResponseStatus.Message) + + require.Nil(c.t, requestReceived.AuthenticationMetadata) + var expectedAuthenticationMetadata *auditinternal.AuthenticationMetadata + if r.expectedCode == http.StatusOK && r.expectedAttemptMode != "legacy" { + expectedAuthenticationMetadata = &auditinternal.AuthenticationMetadata{ + ImpersonationConstraint: "impersonate:" + r.expectedAttemptMode, + } + } + require.Equal(c.t, expectedAuthenticationMetadata, responseComplete.AuthenticationMetadata) +} + +func (c *constrainedImpersonationTest) assertCache(r testRequest) { + rr := require.New(c.t) + + tracker := c.constrainedImpersonationHandler.tracker + idxCacheInternals := tracker.idxCache.cache + + if r.expectedCache == nil { + rr.Zero(idxCacheInternals.Len()) + for _, mode := range tracker.modes { + outer, inner := mode.cachesForTests() + rr.Zero(outer.cache.Len()) + rr.Zero(inner.cache.Len()) + } + return + } + + rr.Equal(len(r.expectedCache.modeIdx), idxCacheInternals.Len()) + for username, expectedVerb := range r.expectedCache.modeIdx { + modeIdx, ok := idxCacheInternals.Get(usernameHash(username)) + rr.True(ok) + actualVerb := tracker.modes[modeIdx.(int)].verbForTests() + rr.Equal(expectedVerb, actualVerb) + } + + for _, mode := range tracker.modes { + verb := mode.verbForTests() + outer, inner := mode.cachesForTests() + expectedMode, ok := r.expectedCache.modes[verb] + if !ok { + rr.Zero(outer.cache.Len()) + rr.Zero(inner.cache.Len()) + continue + } + rr.Equal(len(expectedMode.outer), outer.cache.Len()) + for expectedKey, expectedUser := range expectedMode.outer { + c.checkCacheEntry(outer, expectedKey.wantedUser, expectedKey.attributes, expectedKey.rawKey, expectedUser, verb) + } + + rr.Equal(len(expectedMode.inner), inner.cache.Len()) + for expectedKey, expectedUser := range expectedMode.inner { + c.checkCacheEntry(inner, expectedKey.wantedUser, authorizer.AttributesRecord{User: expectedKey.requestor}, expectedKey.rawKey, expectedUser, verb) + } + } +} + +func usernameHash(username string) string { + hash := fnvSum128a([]byte(username)) + return fmt.Sprintf("%x", hash) +} + +func fnvSum128a(data []byte) []byte { + h := fnv.New128a() + h.Write(data) + var sum [16]byte + return h.Sum(sum[:0]) +} + +func (c *constrainedImpersonationTest) checkCacheEntry(cache *impersonationCache, wantedUser *user.DefaultInfo, attributes authorizer.Attributes, rawKey string, expectedUser *user.DefaultInfo, expectedVerb string) { + rr := require.New(c.t) + keyString, err := buildKey(wantedUser, attributes) + rr.NoError(err) + val, ok := cache.cache.Get(keyString) + rr.True(ok) + impersonatedUser := val.(*impersonatedUserInfo) + rr.Equal(expectedVerb, impersonatedUser.constraint) + rr.Equal(expectedUser, impersonatedUser.user) + rr.Equal(keyHash(rawKey)+"/"+attributes.GetUser().GetName(), keyString, "hash of %q does not match", rawKey) +} + +func keyHash(rawKey string) string { + hash := sha256.Sum256([]byte(rawKey)) + return fmt.Sprintf("%x", hash[:]) +} + +func withConstrainedImpersonationAttributes(a authorizer.AttributesRecord, mode string) authorizer.AttributesRecord { + a.Verb = "impersonate:" + mode + a.APIGroup = "authentication.k8s.io" + a.APIVersion = "v1" + a.ResourceRequest = true + return a +} + +func withImpersonateOnAttributes(requestInfo *request.RequestInfo, mode string) authorizer.AttributesRecord { + a := requestInfoToAttributes(requestInfo) + a.Verb = "impersonate-on:" + mode + ":" + a.Verb + return a +} + +func requestInfoToAttributes(requestInfo *request.RequestInfo) authorizer.AttributesRecord { + requestCtx := request.WithRequestInfo(context.Background(), requestInfo) + attrs, err := filters.GetAuthorizerAttributes(requestCtx) + if err != nil { + panic(err) + } + return *(attrs.(*authorizer.AttributesRecord)) +} + +func withLegacyImpersonateAttributes(a authorizer.AttributesRecord) authorizer.AttributesRecord { + a.Verb = "impersonate" + a.APIVersion = "v1" + a.ResourceRequest = true + return a +} + +func withUser(a authorizer.AttributesRecord, u *user.DefaultInfo) authorizer.AttributesRecord { + a.User = u + return a +} + +func expectAuthzCheck(attributes authorizer.AttributesRecord, decision authorizer.Decision) authzCheck { + return authzCheck{ + attributes: attributes, + decision: decision, + } +} + +func expectAllow(attributes authorizer.AttributesRecord) authzCheck { + return expectAuthzCheck(attributes, authorizer.DecisionAllow) +} + +func expectDeny(attributes authorizer.AttributesRecord) authzCheck { + return expectAuthzCheck(attributes, authorizer.DecisionNoOpinion) +} + +func outerCacheKey(wantedUser, requestor *user.DefaultInfo, req *request.RequestInfo, rawKey string) outerKey { + attributes := withUser(requestInfoToAttributes(req), requestor) + return outerKey{ + wantedUser: wantedUser, + attributes: &attributes, // use a *authorizer.AttributesRecord so that it can be used in a map key + rawKey: rawKey, + } +} + +type expectedCache struct { + modeIdx map[string]string // username -> mode verb + modes map[string]expectedModeCache // mode verb -> cache +} + +type expectedModeCache struct { + outer map[outerKey]*user.DefaultInfo + inner map[innerKey]*user.DefaultInfo +} + +type outerKey struct { + wantedUser *user.DefaultInfo + attributes *authorizer.AttributesRecord + rawKey string +} + +type innerKey struct { + wantedUser, requestor *user.DefaultInfo + rawKey string +} + +type testRequest struct { + request *request.RequestInfo + requestor *user.DefaultInfo + impersonatedUser *user.DefaultInfo + + expectedImpersonatedUser *user.DefaultInfo + expectedMessage string + + expectedAttributesUser *user.DefaultInfo // nil means use requestor + expectedAuthzChecks []authzCheck + expectedCache *expectedCache + expectedCode int + + expectedAttemptMode string + expectedAttemptDecision string + expectedAuthorizationMetrics map[string]int +} + +func associatedNodeTestCase() []testRequest { + getSecretRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "secrets", + Name: "foo", + Namespace: "bar", + } + getPodRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Name: "foo", + Namespace: "bar", + } + saDefaultOnNode1 := &user.DefaultInfo{ + Name: "system:serviceaccount:default:default", + Groups: []string{"associate-node-impersonater"}, + Extra: map[string][]string{ + serviceaccount.NodeNameKey: {"node1"}, + }, + } + saDefaultOnNode2 := &user.DefaultInfo{ + Name: "system:serviceaccount:default:default", + Groups: []string{"associate-node-impersonater"}, + Extra: map[string][]string{ + serviceaccount.NodeNameKey: {"node2"}, + }, + } + saDefaultOnAnyNode := &user.DefaultInfo{ + Name: "system:serviceaccount:default:default", + Groups: []string{"associate-node-impersonater"}, + Extra: map[string][]string{ + "authentication.kubernetes.io/associated-node-keys": {"authentication.kubernetes.io/node-name"}, + }, + } + node1FullUserInfo := &user.DefaultInfo{ + Name: "system:node:node1", + Groups: []string{user.NodesGroup, "system:authenticated"}, + } + node2FullUserInfo := &user.DefaultInfo{ + Name: "system:node:node2", + Groups: []string{user.NodesGroup, "system:authenticated"}, + } + cacheWithOnlyNode1Data := &expectedCache{ + modeIdx: map[string]string{ + "system:serviceaccount:default:default": "impersonate:associated-node", + }, + modes: map[string]expectedModeCache{ + "impersonate:associated-node": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x004\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\asecrets\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{Name: "system:node:*"}, + requestor: saDefaultOnAnyNode, + rawKey: "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: node1FullUserInfo, + }, + }, + }, + } + cacheWithMultipleRequests := &expectedCache{ + modeIdx: cacheWithOnlyNode1Data.modeIdx, + modes: map[string]expectedModeCache{ + "impersonate:associated-node": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getSecretRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x004\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\asecrets\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, + // even though this request was made on node2, since the inner cache matched it returned a value for node1 + outerCacheKey(&user.DefaultInfo{Name: "system:node:*"}, saDefaultOnAnyNode, getPodRequest, + "\x00\x00\x00\x1d\x00\x00\x00\rsystem:node:*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb7\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x1bassociate-node-impersonater\x00\x00\x00c\x00\x00\x001authentication.kubernetes.io/associated-node-keys\x00\x00\x00*\x00\x00\x00&authentication.kubernetes.io/node-name\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): node1FullUserInfo, + }, + inner: cacheWithOnlyNode1Data.modes["impersonate:associated-node"].inner, + }, + }, + } + return []testRequest{ + { + request: getSecretRequest, + requestor: saDefaultOnNode1, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches + expectedImpersonatedUser: node1FullUserInfo, + expectedAttributesUser: saDefaultOnAnyNode, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getSecretRequest, "associated-node")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "*"}, "associated-node")), + }, + expectedCache: cacheWithOnlyNode1Data, + expectedCode: http.StatusOK, + expectedAttemptMode: "associated-node", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "associated-node/allowed": 2, // impersonate-on + impersonate:associated-node + }, + }, + { + request: getSecretRequest, + requestor: saDefaultOnNode2, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches + expectedImpersonatedUser: node2FullUserInfo, + expectedAttributesUser: saDefaultOnAnyNode, + expectedAuthzChecks: nil, // no authz checks for the second request + expectedCache: cacheWithOnlyNode1Data, + expectedCode: http.StatusOK, + expectedAttemptMode: "associated-node", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: nil, // full cache hit + }, + { + request: getSecretRequest, + requestor: saDefaultOnNode2, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node does not match + expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "system:serviceaccount:default:default" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`, + expectedAttributesUser: nil, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getSecretRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), + }, + expectedCache: cacheWithOnlyNode1Data, + expectedCode: http.StatusForbidden, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get + "arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes + "legacy/denied": 1, // impersonate users + }, + }, + { + request: getPodRequest, + requestor: saDefaultOnNode2, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node2"}, // node matches + expectedImpersonatedUser: node2FullUserInfo, + expectedAttributesUser: saDefaultOnAnyNode, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "associated-node")), // one authz check because different request info + }, + expectedCache: cacheWithMultipleRequests, + expectedCode: http.StatusOK, + expectedAttemptMode: "associated-node", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "associated-node/allowed": 1, // impersonate-on only (inner cache hit) + }, + }, + { + request: getPodRequest, + requestor: saDefaultOnNode1, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, // node matches + expectedImpersonatedUser: node1FullUserInfo, + expectedAttributesUser: nil, + expectedAuthzChecks: nil, // no authz checks for pod request via node1 + expectedCache: cacheWithMultipleRequests, + expectedCode: http.StatusOK, + expectedAttemptMode: "associated-node", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: nil, // full cache hit + }, + } +} + +type impersonationTestCase struct { + name string + requests []testRequest +} + +func constrainedImpersonationTestCases() []impersonationTestCase { + getPodRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Name: "foo", + Namespace: "bar", + } + + getAnotherPodRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Name: "foo1", + Namespace: "bar1", + } + + createPodRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "create", + APIVersion: "v1", + Resource: "pods", + Name: "foo", + Namespace: "bar", + } + + getDeploymentRequest := &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + APIGroup: "apps", + Resource: "deployments", + Name: "foo", + Namespace: "bar", + } + + anyone := &user.DefaultInfo{Name: "anyone"} + anyoneAuthenticated := &user.DefaultInfo{Name: "anyone", Groups: []string{"system:authenticated"}} + saUser := &user.DefaultInfo{Name: "system:serviceaccount:default:default"} + saUserAuthenticated := &user.DefaultInfo{ + Name: "system:serviceaccount:default:default", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}} + nodeUser := &user.DefaultInfo{Name: "system:node:node1"} + nodeUserAuthenticated := &user.DefaultInfo{ + Name: "system:node:node1", + Groups: []string{user.NodesGroup, "system:authenticated"}, + } + + userImpersonator := &user.DefaultInfo{Name: "user-impersonater"} + saImpersonator := &user.DefaultInfo{Name: "sa-impersonater"} + nodeImpersonator := &user.DefaultInfo{Name: "node-impersonater"} + + testCases := []impersonationTestCase{ + { + name: "impersonating-error", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "tester"}, + impersonatedUser: anyone, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `users.authentication.k8s.io "anyone" is forbidden: User "tester" cannot impersonate:user-info resource "users" in API group "authentication.k8s.io" at the cluster scope: deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 1, // impersonate-on:user-info:get + "user-info/denied": 1, // impersonate:user-info users + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "impersonating-user-get-allowed-create-disallowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + { + request: getAnotherPodRequest, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getAnotherPodRequest, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + outerCacheKey(anyone, userImpersonator, getAnotherPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x03get\x01\x00\x00\x00\x04bar1\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x04foo1\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 1, // impersonate-on (inner cache hit skips impersonate:user-info) + }, + }, + { + request: createPodRequest, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedAuthzChecks: []authzCheck{ + expectDeny(withImpersonateOnAttributes(createPodRequest, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, getPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + outerCacheKey(anyone, userImpersonator, getAnotherPodRequest, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\x00\x03get\x01\x00\x00\x00\x04bar1\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x04foo1\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusForbidden, + expectedMessage: `pods "foo" is forbidden: User "user-impersonater" cannot impersonate-on:user-info:create resource "pods" in API group "" in the namespace "bar": deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "user-info/denied": 1, // impersonate-on:user-info:create + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "impersonating-sa-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: saImpersonator, + impersonatedUser: saUser, + expectedImpersonatedUser: saUserAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "serviceaccount")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "serviceaccounts", Namespace: "default", Name: "default"}, "serviceaccount")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + saImpersonator.Name: "impersonate:serviceaccount", + }, + modes: map[string]expectedModeCache{ + "impersonate:serviceaccount": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(saUser, saImpersonator, getPodRequest, + "\x00\x00\x005\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x0fsa-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): saUserAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: saUser, + requestor: saImpersonator, + rawKey: "\x00\x00\x005\x00\x00\x00%system:serviceaccount:default:default\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x0fsa-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: saUserAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "serviceaccount", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "serviceaccount/allowed": 2, // impersonate-on + impersonate:serviceaccount + }, + }, + }, + }, + { + name: "impersonating-node-not-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: saImpersonator, + impersonatedUser: nodeUser, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "sa-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get + "arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "impersonating-node-not-allowed-action", + requests: []testRequest{ + { + request: createPodRequest, + requestor: nodeImpersonator, + impersonatedUser: nodeUser, + expectedAuthzChecks: []authzCheck{ + expectDeny(withImpersonateOnAttributes(createPodRequest, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `pods "foo" is forbidden: User "node-impersonater" cannot impersonate-on:arbitrary-node:create resource "pods" in API group "" in the namespace "bar": deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "arbitrary-node/denied": 1, // impersonate-on:arbitrary-node:create + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "impersonating-node-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: nodeImpersonator, + impersonatedUser: nodeUser, + expectedImpersonatedUser: nodeUserAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + nodeImpersonator.Name: "impersonate:arbitrary-node", + }, + modes: map[string]expectedModeCache{ + "impersonate:arbitrary-node": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(nodeUser, nodeImpersonator, getPodRequest, + "\x00\x00\x00!\x00\x00\x00\x11system:node:node1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11node-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): nodeUserAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: nodeUser, + requestor: nodeImpersonator, + rawKey: "\x00\x00\x00!\x00\x00\x00\x11system:node:node1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11node-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: nodeUserAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "arbitrary-node", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "arbitrary-node/allowed": 2, // impersonate-on + impersonate:arbitrary-node + }, + }, + }, + }, + { + name: "disallowed-userextra-3", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "user-impersonater", + Groups: []string{"group-impersonater"}, + }, + impersonatedUser: &user.DefaultInfo{ + Name: "system:admin", + Groups: []string{"extra-setter-scopes"}, + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b"}}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "extra-setter-scopes"}, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `userextras.authentication.k8s.io "scope-a" is forbidden: User "user-impersonater" cannot impersonate:user-info resource "userextras/pandas.io/scopes" in API group "authentication.k8s.io" at the cluster scope: deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on:get + users + groups + "user-info/denied": 1, // userextras scope-a + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "allowed-userextras", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "user-impersonater", + Groups: []string{"extra-setter-scopes"}, + }, + impersonatedUser: &user.DefaultInfo{ + Name: "system:admin", + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:admin", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "*", Name: "*"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-a"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "scope-b"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "5"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "4"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "3"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "2"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "pandas.io/scopes", Name: "1"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey( + &user.DefaultInfo{ + Name: "system:admin", + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + &user.DefaultInfo{ + Name: "user-impersonater", + Groups: []string{"extra-setter-scopes"}, + }, + getPodRequest, "\x00\x00\x00c\x00\x00\x00\fsystem:admin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00G\x00\x00\x00\x10pandas.io/scopes\x00\x00\x00/\x00\x00\x00\ascope-a\x00\x00\x00\ascope-b\x00\x00\x00\x015\x00\x00\x00\x014\x00\x00\x00\x013\x00\x00\x00\x012\x00\x00\x00\x011\x00\x00\x008\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x13extra-setter-scopes\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"): { + Name: "system:admin", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "system:admin", + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + requestor: &user.DefaultInfo{ + Name: "user-impersonater", + Groups: []string{"extra-setter-scopes"}, + }, + rawKey: "\x00\x00\x00c\x00\x00\x00\fsystem:admin\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00G\x00\x00\x00\x10pandas.io/scopes\x00\x00\x00/\x00\x00\x00\ascope-a\x00\x00\x00\ascope-b\x00\x00\x00\x015\x00\x00\x00\x014\x00\x00\x00\x013\x00\x00\x00\x012\x00\x00\x00\x011\x00\x00\x008\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x13extra-setter-scopes\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "system:admin", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{"pandas.io/scopes": {"scope-a", "scope-b", "5", "4", "3", "2", "1"}}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 9, // impersonate-on:get + users + 7 individual extra values + "user-info/denied": 1, // wildcard userextras check + }, + }, + }, + }, + { + name: "allowed-associate-node", + requests: associatedNodeTestCase(), + }, + { + name: "disallowed-associate-node-without-sa", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "user-impersonater", + Groups: []string{"associate-node-impersonater"}, + Extra: map[string][]string{ + serviceaccount.NodeNameKey: {"node1"}, + }, + }, + impersonatedUser: &user.DefaultInfo{Name: "system:node:node1"}, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "arbitrary-node")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "nodes", Name: "node1"}, "arbitrary-node")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:node:node1"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `nodes.authentication.k8s.io "node1" is forbidden: User "user-impersonater" cannot impersonate:arbitrary-node resource "nodes" in API group "authentication.k8s.io" at the cluster scope: deny by default`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "arbitrary-node/allowed": 1, // impersonate-on:arbitrary-node:get + "arbitrary-node/denied": 1, // impersonate:arbitrary-node nodes + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "allowed-legacy-impersonator", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "legacy-impersonater"}, + impersonatedUser: &user.DefaultInfo{Name: "system:admin"}, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:admin", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"}, "user-info")), + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "system:admin"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "legacy-impersonater": "impersonate", + }, + modes: nil, // legacy impersonation does not cache + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "legacy", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 1, // impersonate-on:user-info:get + "user-info/denied": 1, // impersonate:user-info users + "legacy/allowed": 1, // impersonate users + }, + }, + }, + }, + { + name: "system:masters-group-not-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: userImpersonator, + impersonatedUser: &user.DefaultInfo{ + Name: "admin-user", + Groups: []string{user.SystemPrivilegedGroup}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "admin-user"}, "user-info")), + expectDeny(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "admin-user"})), + }, + expectedCache: nil, + expectedCode: http.StatusForbidden, + expectedMessage: `groups.authentication.k8s.io "system:masters" is forbidden: User "user-impersonater" cannot impersonate:user-info resource "groups" in API group "authentication.k8s.io" at the cluster scope: impersonating the system:masters group is not allowed`, + expectedAttemptMode: "", + expectedAttemptDecision: "denied", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on:user-info:get, impersonate:user-info users + "legacy/denied": 1, // impersonate users + }, + }, + }, + }, + { + name: "many-groups-wildcard-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "many-groups-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "bob"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "*"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "many-groups-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, &user.DefaultInfo{Name: "many-groups-impersonater"}, getPodRequest, + "\x00\x00\x00;\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06group1\x00\x00\x00\x06group2\x00\x00\x00\x06group3\x00\x00\x00\x06group4\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x18many-groups-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4"}, + }, + requestor: &user.DefaultInfo{Name: "many-groups-impersonater"}, + rawKey: "\x00\x00\x00;\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06group1\x00\x00\x00\x06group2\x00\x00\x00\x06group3\x00\x00\x00\x06group4\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x18many-groups-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "bob", + Groups: []string{"group1", "group2", "group3", "group4", "system:authenticated"}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on:user-info:get, impersonate:user-info users/groups + }, + }, + }, + }, + { + name: "many-extras-wildcard-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "many-extras-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "alice"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "*", Name: "*"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "many-extras-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, &user.DefaultInfo{Name: "many-extras-impersonater"}, getPodRequest, + "\x00\x00\x00\xb1\x00\x00\x00\x05alice\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9c\x00\x00\x00\x17scopes.example.com/key1\x00\x00\x00\b\x00\x00\x00\x04val1\x00\x00\x00\x17scopes.example.com/key2\x00\x00\x00\b\x00\x00\x00\x04val2\x00\x00\x00\x17scopes.example.com/key3\x00\x00\x00\b\x00\x00\x00\x04val3\x00\x00\x00\x17scopes.example.com/key4\x00\x00\x00\b\x00\x00\x00\x04val4\x00\x00\x00(\x00\x00\x00\x18many-extras-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "alice", + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + requestor: &user.DefaultInfo{Name: "many-extras-impersonater"}, + rawKey: "\x00\x00\x00\xb1\x00\x00\x00\x05alice\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9c\x00\x00\x00\x17scopes.example.com/key1\x00\x00\x00\b\x00\x00\x00\x04val1\x00\x00\x00\x17scopes.example.com/key2\x00\x00\x00\b\x00\x00\x00\x04val2\x00\x00\x00\x17scopes.example.com/key3\x00\x00\x00\b\x00\x00\x00\x04val3\x00\x00\x00\x17scopes.example.com/key4\x00\x00\x00\b\x00\x00\x00\x04val4\x00\x00\x00(\x00\x00\x00\x18many-extras-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "alice", + Groups: []string{"system:authenticated"}, + Extra: map[string][]string{ + "scopes.example.com/key1": {"val1"}, + "scopes.example.com/key2": {"val2"}, + "scopes.example.com/key3": {"val3"}, + "scopes.example.com/key4": {"val4"}, + }, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on:user-info:get, impersonate:user-info users/extras + }, + }, + }, + }, + { + name: "mixed-groups-extras-wildcard-denied-individuals-allowed", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{ + Name: "mixed-impersonater", + }, + impersonatedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + expectedAuthzChecks: []authzCheck{ + // impersonate-on check passes + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + // user check passes + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "frank"}, "user-info")), + // wildcard groups check FAILS (mixed-impersonater doesn't allow wildcard groups) + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "*"}, "user-info")), + // individual group checks PASS - demonstrating fallback after wildcard failure + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "dev"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "ops"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "security"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "groups", Name: "audit"}, "user-info")), + // individual extra checks PASS (wildcard not attempted for extras with single subresource) + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "prod"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "staging"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "userextras", Subresource: "tags.io/env", Name: "dev"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "mixed-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, &user.DefaultInfo{Name: "mixed-impersonater"}, getPodRequest, + "\x00\x00\x00e\x00\x00\x00\x05frank\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x03dev\x00\x00\x00\x03ops\x00\x00\x00\bsecurity\x00\x00\x00\x05audit\x00\x00\x00-\x00\x00\x00\vtags.io/env\x00\x00\x00\x1a\x00\x00\x00\x04prod\x00\x00\x00\astaging\x00\x00\x00\x03dev\x00\x00\x00\"\x00\x00\x00\x12mixed-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{ + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + requestor: &user.DefaultInfo{Name: "mixed-impersonater"}, + rawKey: "\x00\x00\x00e\x00\x00\x00\x05frank\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x03dev\x00\x00\x00\x03ops\x00\x00\x00\bsecurity\x00\x00\x00\x05audit\x00\x00\x00-\x00\x00\x00\vtags.io/env\x00\x00\x00\x1a\x00\x00\x00\x04prod\x00\x00\x00\astaging\x00\x00\x00\x03dev\x00\x00\x00\"\x00\x00\x00\x12mixed-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "frank", + Groups: []string{"dev", "ops", "security", "audit", "system:authenticated"}, + Extra: map[string][]string{ + "tags.io/env": {"prod", "staging", "dev"}, + }, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 9, // impersonate-on:user-info:get; impersonate:user-info users; 4*impersonate:user-info groups; 3*impersonate:user-info extras + "user-info/denied": 1, // impersonate:user-info groups "*" + }, + }, + }, + }, + { + name: "continuous-same-allowed-user-requests", + requests: []testRequest{ + { + request: getDeploymentRequest, + requestor: userImpersonator, + impersonatedUser: &user.DefaultInfo{Name: "bob"}, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getDeploymentRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "bob"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest, + "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\vdeployments\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x04apps\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{Name: "bob"}, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + { + request: getDeploymentRequest, + requestor: userImpersonator, + impersonatedUser: &user.DefaultInfo{Name: "bob"}, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: nil, // no authz checks for cached request + expectedCode: http.StatusOK, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(&user.DefaultInfo{Name: "bob"}, userImpersonator, getDeploymentRequest, + "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\vdeployments\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x04apps\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{Name: "bob"}, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x13\x00\x00\x00\x03bob\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "bob", + Groups: []string{"system:authenticated"}, + }, + }, + }, + }, + }, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: nil, // full cache hit + }, + }, + }, + { + name: "impersonating-user-with-uid", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + impersonatedUser: &user.DefaultInfo{ + Name: "anyone", + UID: "wanted-uid-456", + }, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "uids", Name: "wanted-uid-456"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "user-impersonater": "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey( + &user.DefaultInfo{Name: "anyone", UID: "wanted-uid-456"}, + &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + getPodRequest, + "\x00\x00\x00$\x00\x00\x00\x06anyone\x00\x00\x00\x0ewanted-uid-456\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x11user-impersonater\x00\x00\x00\x11requestor-uid-123\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): { + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: &user.DefaultInfo{Name: "anyone", UID: "wanted-uid-456"}, + requestor: &user.DefaultInfo{Name: "user-impersonater", UID: "requestor-uid-123"}, + rawKey: "\x00\x00\x00$\x00\x00\x00\x06anyone\x00\x00\x00\x0ewanted-uid-456\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\x00\x11user-impersonater\x00\x00\x00\x11requestor-uid-123\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: { + Name: "anyone", + UID: "wanted-uid-456", + Groups: []string{"system:authenticated"}, + }, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 3, // impersonate-on + impersonate:user-info users + uids + }, + }, + }, + }, + { + name: "impersonating-user-subresource-request", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + Subresource: "status", + Name: "foo", + Namespace: "bar", + }, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\x00\x03get\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x06status\x00\x00\x00\x03foo\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "impersonating-user-non-resource-request", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: false, + Verb: "get", + Path: "/healthz", + }, + "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\x00\x03get\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b/healthz\x00\x00\x00\x00\x00\x00\x00\x00", + ): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "impersonating-user-with-field-and-label-selectors", + requests: []testRequest{ + { + request: &request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, + requestor: userImpersonator, + impersonatedUser: anyone, + expectedImpersonatedUser: anyoneAuthenticated, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(&request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, "user-info")), + expectAllow(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + userImpersonator.Name: "impersonate:user-info", + }, + modes: map[string]expectedModeCache{ + "impersonate:user-info": { + outer: map[outerKey]*user.DefaultInfo{ + outerCacheKey(anyone, userImpersonator, &request.RequestInfo{ + IsResourceRequest: true, + Verb: "list", + APIVersion: "v1", + Resource: "pods", + Namespace: "bar", + FieldSelector: "spec.nodeName=node1", + LabelSelector: "app=web", + }, "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00\x00\x00\x04list\x01\x00\x00\x00\x03bar\x00\x00\x00\x04pods\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02v1\x01\x00\x00\x00\x00\x00\x00\x00#\x00\x00\x00\x1f\x00\x00\x00\rspec.nodeName\x00\x00\x00\x01=\x00\x00\x00\x05node1\x00\x00\x00\v\x00\x00\x00\aapp=web"): anyoneAuthenticated, + }, + inner: map[innerKey]*user.DefaultInfo{ + { + wantedUser: anyone, + requestor: userImpersonator, + rawKey: "\x00\x00\x00\x16\x00\x00\x00\x06anyone\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x00\x00\x11user-impersonater\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + }: anyoneAuthenticated, + }, + }, + }, + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "user-info", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 2, // impersonate-on + impersonate:user-info + }, + }, + }, + }, + { + name: "legacy-fallback", + requests: []testRequest{ + { + request: getPodRequest, + requestor: &user.DefaultInfo{Name: "legacy-impersonater"}, + impersonatedUser: anyone, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withImpersonateOnAttributes(getPodRequest, "user-info")), + expectDeny(withConstrainedImpersonationAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"}, "user-info")), + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "legacy-impersonater": "impersonate", + }, + modes: nil, // legacy impersonation does not cache + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "legacy", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "user-info/allowed": 1, // impersonate-on:user-info:get + "user-info/denied": 1, // impersonate:user-info users + "legacy/allowed": 1, // impersonate users + }, + }, + { + request: getDeploymentRequest, + requestor: &user.DefaultInfo{Name: "legacy-impersonater"}, + impersonatedUser: anyone, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "anyone", + Groups: []string{"system:authenticated"}, + }, + expectedAuthzChecks: []authzCheck{ + expectAllow(withLegacyImpersonateAttributes(authorizer.AttributesRecord{Resource: "users", Name: "anyone"})), + }, + expectedCache: &expectedCache{ + modeIdx: map[string]string{ + "legacy-impersonater": "impersonate", + }, + modes: nil, // legacy impersonation does not cache + }, + expectedCode: http.StatusOK, + expectedAttemptMode: "legacy", + expectedAttemptDecision: "allowed", + expectedAuthorizationMetrics: map[string]int{ + "legacy/allowed": 1, // impersonate users (mode index cache hit skips constrained checks) + }, + }, + }, + }, + } + return testCases +} + +func TestConstrainedImpersonationFilter(t *testing.T) { + testCases := constrainedImpersonationTestCases() + + var mux http.ServeMux + tests := make([]*constrainedImpersonationTest, len(testCases)) + handlers := make([]http.Handler, len(testCases)) + for i := range len(testCases) { + mux.Handle("/"+strconv.Itoa(i), http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + handlers[i].ServeHTTP(w, req) + })) + } + + server := httptest.NewServer(&mux) + t.Cleanup(server.Close) + + for i, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + test := &constrainedImpersonationTest{t: t} + tests[i] = test + handlers[i] = test.handler() + + for _, r := range tc.requests { + resp := doImpersonationRequest(t, http.DefaultTransport, server.URL+"/"+strconv.Itoa(i), r, r.expectedCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + _ = resp.Body.Close() + + if r.expectedCode == http.StatusOK { + var actualUser user.DefaultInfo + if err := json.Unmarshal(body, &actualUser); err != nil { + t.Errorf("unexpected error: %v, body=\n%s", err, string(body)) + } + + require.Equal(t, r.expectedImpersonatedUser, &actualUser) + test.assertEchoCalled(true) + require.NotNil(t, r.expectedImpersonatedUser) // sanity check test data + require.Empty(t, r.expectedMessage) // sanity check test data + } else { + var status metav1.Status + if err := json.Unmarshal(body, &status); err != nil { + t.Errorf("unexpected error: %v, body=\n%s", err, string(body)) + } + require.Equal(t, r.expectedMessage, status.Message) + test.assertEchoCalled(false) + require.NotEmpty(t, r.expectedMessage) // sanity check test data + require.Nil(t, r.expectedImpersonatedUser) // sanity check test data + } + + test.assertAttributes(r) + test.assertCache(r) + test.assertMetrics(r) + test.assertAuditEvents(r) + } + }) + } +} + +func TestConstrainedImpersonationParallel(t *testing.T) { + // This test verifies goroutine safety by sending concurrent requests. + // Each test case defines multiple requests that are sent in parallel, + // ensuring no data races or request interference occurs. + + // Common request infos + getPods := &request.RequestInfo{IsResourceRequest: true, Verb: "get", APIVersion: "v1", Resource: "pods"} + listServices := &request.RequestInfo{IsResourceRequest: true, Verb: "list", APIVersion: "v1", Resource: "services"} + createConfigMaps := &request.RequestInfo{IsResourceRequest: true, Verb: "create", APIVersion: "v1", Resource: "configmaps"} + createSecrets := &request.RequestInfo{IsResourceRequest: true, Verb: "create", APIVersion: "v1", Resource: "secrets"} + + type parallelRequest struct { + requestor *user.DefaultInfo + impersonatedUser string + requestInfo *request.RequestInfo + expectedStatusCode int + expectedImpersonatedUser *user.DefaultInfo + } + + testCases := []struct { + name string + requests []parallelRequest + }{ + { + name: "different-users-same-target", + requests: []parallelRequest{ + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:my-sa", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "node-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusForbidden, + }, + { + requestor: &user.DefaultInfo{Name: "user-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusForbidden, + }, + }, + }, + { + name: "same-user-same-targets", + requests: []parallelRequest{ + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:my-sa", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:my-sa", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + }, + }, + { + name: "same-user-different-targets", + requests: []parallelRequest{ + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:sa1", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:sa1", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:kube-system:sa2", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:kube-system:sa2", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:kube-system", "system:authenticated"}, + Extra: map[string][]string{}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:node:node-1", + requestInfo: getPods, + expectedStatusCode: http.StatusForbidden, + }, + }, + }, + { + name: "same-user-same-target-different-operations", + requests: []parallelRequest{ + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:my-sa", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: listServices, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:my-sa", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: createConfigMaps, + expectedStatusCode: http.StatusForbidden, + }, + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:my-sa", + requestInfo: createSecrets, + expectedStatusCode: http.StatusForbidden, + }, + }, + }, + { + name: "mixed-scenarios", + requests: []parallelRequest{ + { + requestor: &user.DefaultInfo{Name: "sa-impersonater"}, + impersonatedUser: "system:serviceaccount:default:sa1", + requestInfo: getPods, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:serviceaccount:default:sa1", + Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default", "system:authenticated"}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "node-impersonater"}, + impersonatedUser: "system:node:node-1", + requestInfo: listServices, + expectedStatusCode: http.StatusOK, + expectedImpersonatedUser: &user.DefaultInfo{ + Name: "system:node:node-1", + Groups: []string{"system:nodes", "system:authenticated"}, + Extra: map[string][]string{}, + }, + }, + { + requestor: &user.DefaultInfo{Name: "user-impersonater"}, + impersonatedUser: "alice", + requestInfo: createConfigMaps, + expectedStatusCode: http.StatusForbidden, + }, + { + requestor: &user.DefaultInfo{Name: "unauthorized-user"}, + impersonatedUser: "bob", + requestInfo: getPods, + expectedStatusCode: http.StatusForbidden, + }, + }, + }, + } + + type resultType struct { + requestIdx int + statusCode int + body []byte + err error + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + test := &constrainedImpersonationTest{t: t} + server := httptest.NewServer(test.parallelHandler()) + defer server.Close() + + var wg sync.WaitGroup + results := make(chan resultType, len(tc.requests)) + + // Send all requests concurrently + for idx, req := range tc.requests { + wg.Go(func() { + rt := &testRoundTripper{ + user: req.requestor, + requestInfo: req.requestInfo, + delegate: http.DefaultTransport, + } + client := &http.Client{Transport: rt} + + httpReq, err := http.NewRequest(http.MethodGet, server.URL, nil) + if err != nil { + results <- resultType{requestIdx: idx, err: err} + return + } + + if len(req.impersonatedUser) > 0 { + httpReq.Header.Add(authenticationv1.ImpersonateUserHeader, req.impersonatedUser) + } + + resp, err := client.Do(httpReq) + if err != nil { + results <- resultType{requestIdx: idx, err: err} + return + } + defer func() { + err := resp.Body.Close() + if err != nil { + results <- resultType{requestIdx: idx, err: err} + } + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + results <- resultType{requestIdx: idx, err: err} + return + } + + results <- resultType{ + requestIdx: idx, + statusCode: resp.StatusCode, + body: body, + } + }) + } + + // Wait for all requests to complete + wg.Wait() + close(results) + + // Collect results + resultMap := make(map[int]resultType) + for res := range results { + require.NoError(t, res.err, "request %d: unexpected error", res.requestIdx) + _, ok := resultMap[res.requestIdx] + require.False(t, ok, "request %d: should not duplicate in the result", res.requestIdx) + resultMap[res.requestIdx] = res + } + + // Verify each request's result + for idx, req := range tc.requests { + res, ok := resultMap[idx] + require.True(t, ok, "request %d: missing result", idx) + + require.Equal(t, req.expectedStatusCode, res.statusCode, + "request %d: status code mismatch (requestor: %s, target: %s, verb: %s, resource: %s)", + idx, req.requestor.Name, req.impersonatedUser, req.requestInfo.Verb, req.requestInfo.Resource) + + if req.expectedImpersonatedUser != nil { + // Success case - verify impersonated user + var actualUser user.DefaultInfo + err := json.Unmarshal(res.body, &actualUser) + require.NoError(t, err, "request %d: failed to unmarshal user", idx) + require.Equal(t, req.expectedImpersonatedUser.Name, actualUser.Name, + "request %d: user name mismatch", idx) + require.ElementsMatch(t, req.expectedImpersonatedUser.Groups, actualUser.Groups, + "request %d: groups mismatch", idx) + } + } + }) + } +} + +func (c *constrainedImpersonationTest) benchmarkHandler(withImpersonation func(http.Handler, authorizer.Authorizer, runtime.NegotiatedSerializer) http.Handler) http.Handler { + s := runtime.NewScheme() + metav1.AddToGroupVersion(s, metav1.SchemeGroupVersion) + addImpersonation := withImpersonation(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {}), c, serializer.NewCodecFactory(s)) + h := addImpersonation.(*constrainedImpersonationHandler) + h.recordAttempt = func(ctx context.Context, mode, decision string, _ time.Duration) {} + h.metricsAuthorizer.recordAuthorizationCall = func(mode, decision string, _ time.Duration) {} + addAuthentication := c.authenticationHandler(addImpersonation) + addRequestInfo := c.requestInfoHandler(addAuthentication) + return addRequestInfo +} + +func BenchmarkImpersonation(b *testing.B) { + testCases := constrainedImpersonationTestCases() + + type impersonationHandler struct { + name string + fn func(http.Handler, authorizer.Authorizer, runtime.NegotiatedSerializer) http.Handler + isLegacy bool + } + + impersonators := []impersonationHandler{ + { + name: "constrained", + fn: WithConstrainedImpersonation, + }, + { + name: "legacy", + fn: WithImpersonation, + isLegacy: true, + }, + } + + for _, imp := range impersonators { + b.Run(imp.name, func(b *testing.B) { + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + test := &constrainedImpersonationTest{t: b} + handler := test.benchmarkHandler(imp.fn) + + server := httptest.NewServer(handler) + defer server.Close() + + baseTransport := &http.Transport{ + DisableKeepAlives: true, + } + + b.ResetTimer() + for b.Loop() { + for _, r := range tc.requests { + resp := doImpersonationRequest(b, baseTransport, server.URL, r, benchmarkExpectedCode(r, imp.isLegacy)) + _ = resp.Body.Close() + } + } + b.StopTimer() + }) + } + }) + } +} + +func benchmarkExpectedCode(r testRequest, isLegacy bool) int { + if isLegacy && r.expectedCode == http.StatusOK && r.requestor.Name != "legacy-impersonater" { + // legacy WithImpersonation denies any requestor that only has constrained impersonation verbs + return http.StatusForbidden + } + return r.expectedCode +} + +func doImpersonationRequest(tb testing.TB, baseTransport http.RoundTripper, url string, r testRequest, expectedCode int) *http.Response { + tb.Helper() + + client := &http.Client{ + Transport: &testRoundTripper{ + user: r.requestor, + requestInfo: r.request, + delegate: transport.NewImpersonatingRoundTripper( + transport.ImpersonationConfig{ + UserName: r.impersonatedUser.Name, + UID: r.impersonatedUser.UID, + Groups: r.impersonatedUser.Groups, + Extra: r.impersonatedUser.Extra, + }, + baseTransport, + ), + }, + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + require.NoError(tb, err) + + resp, err := client.Do(req) + require.NoError(tb, err) + require.Equal(tb, expectedCode, resp.StatusCode) + + return resp +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation.go index aa47a7536..f0f7512c7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package filters +package impersonation import ( "errors" @@ -27,6 +27,7 @@ import ( authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authentication/serviceaccount" @@ -43,7 +44,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime. impersonationRequests, err := buildImpersonationRequests(req.Header) if err != nil { klog.V(4).Infof("%v", err) - responsewriters.InternalError(w, req, err) + responsewriters.RespondWithError(w, req, err, s) return } if len(impersonationRequests) == 0 { @@ -110,14 +111,14 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime. default: klog.V(4).InfoS("unknown impersonation request type", "request", impersonationRequest) - responsewriters.Forbidden(ctx, actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s) + responsewriters.Forbidden(actingAsAttributes, w, req, fmt.Sprintf("unknown impersonation request type: %v", impersonationRequest), s) return } decision, reason, err := a.Authorize(ctx, actingAsAttributes) if err != nil || decision != authorizer.DecisionAllow { klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "reason", reason, "err", err) - responsewriters.Forbidden(ctx, actingAsAttributes, w, req, reason, s) + responsewriters.Forbidden(actingAsAttributes, w, req, reason, s) return } } @@ -166,7 +167,7 @@ func WithImpersonation(handler http.Handler, a authorizer.Authorizer, s runtime. oldUser, _ := request.UserFrom(ctx) httplog.LogOf(req, w).Addf("%v is impersonating %v", userString(oldUser), userString(newUser)) - audit.LogImpersonatedUser(audit.WithAuditContext(ctx), newUser) + audit.LogImpersonatedUser(audit.WithAuditContext(ctx), newUser, "") // clear all the impersonation headers from the request req.Header.Del(authenticationv1.ImpersonateUserHeader) @@ -266,7 +267,7 @@ func buildImpersonationRequests(headers http.Header) ([]v1.ObjectReference, erro } if (hasGroups || hasUserExtra || hasUID) && !hasUser { - return nil, fmt.Errorf("requested %v without impersonating a user", impersonationRequests) + return nil, apierrors.NewBadRequest(fmt.Sprintf("requested %v without impersonating a user", impersonationRequests)) } return impersonationRequests, nil diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation_test.go similarity index 87% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation_test.go index 241486926..1d586d64e 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/impersonation_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/impersonation_test.go @@ -14,19 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package filters +package impersonation import ( "context" "fmt" "net/http" "net/http/httptest" - "reflect" "strings" "sync" "testing" authenticationapi "k8s.io/api/authentication/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" serializer "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/authentication/user" @@ -143,7 +143,7 @@ func TestImpersonationFilter(t *testing.T) { expectedUser: &user.DefaultInfo{ Name: "tester", }, - expectedCode: http.StatusInternalServerError, + expectedCode: http.StatusBadRequest, }, { name: "impersonating-extra-without-user", @@ -154,7 +154,7 @@ func TestImpersonationFilter(t *testing.T) { expectedUser: &user.DefaultInfo{ Name: "tester", }, - expectedCode: http.StatusInternalServerError, + expectedCode: http.StatusBadRequest, }, { name: "impersonating-uid-without-user", @@ -165,7 +165,7 @@ func TestImpersonationFilter(t *testing.T) { expectedUser: &user.DefaultInfo{ Name: "tester", }, - expectedCode: http.StatusInternalServerError, + expectedCode: http.StatusBadRequest, }, { name: "disallowed-group", @@ -497,7 +497,7 @@ func TestImpersonationFilter(t *testing.T) { } }) - handler := func(delegate http.Handler) http.Handler { + delegateHandler := func(delegate http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { defer func() { if r := recover(); r != nil { @@ -519,53 +519,66 @@ func TestImpersonationFilter(t *testing.T) { delegate.ServeHTTP(w, req) }) - }(WithImpersonation(doNothingHandler, impersonateAuthorizer{}, serializer.NewCodecFactory(runtime.NewScheme()))) - - server := httptest.NewServer(handler) - defer server.Close() + } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - func() { - lock.Lock() - defer lock.Unlock() - ctx = request.WithUser(request.NewContext(), tc.user) - }() + handlersToTest := map[string]http.Handler{ + "impersonation": delegateHandler(WithImpersonation(doNothingHandler, impersonateAuthorizer{}, serializer.NewCodecFactory(runtime.NewScheme()))), + "constrainedImpersonation": delegateHandler(WithConstrainedImpersonation(doNothingHandler, impersonateAuthorizer{}, serializer.NewCodecFactory(runtime.NewScheme()))), + } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) - if err != nil { - t.Errorf("%s: unexpected error: %v", tc.name, err) - return - } - if len(tc.impersonationUser) > 0 { - req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) - } - for _, group := range tc.impersonationGroups { - req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) - } - for extraKey, values := range tc.impersonationUserExtras { - for _, value := range values { - req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value) + for name, handler := range handlersToTest { + server := httptest.NewServer(handler) + + for _, tc := range testCases { + t.Run(name+"/"+tc.name, func(t *testing.T) { + func() { + lock.Lock() + defer lock.Unlock() + ctx = request.WithUser(request.NewContext(), tc.user) + ctx = request.WithRequestInfo(ctx, &request.RequestInfo{ + IsResourceRequest: true, + Verb: "get", + APIVersion: "v1", + Resource: "pods", + }) + }() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) + if err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + return + } + if len(tc.impersonationUser) > 0 { + req.Header.Add(authenticationapi.ImpersonateUserHeader, tc.impersonationUser) + } + for _, group := range tc.impersonationGroups { + req.Header.Add(authenticationapi.ImpersonateGroupHeader, group) + } + for extraKey, values := range tc.impersonationUserExtras { + for _, value := range values { + req.Header.Add(authenticationapi.ImpersonateUserExtraHeaderPrefix+extraKey, value) + } + } + if len(tc.impersonationUid) > 0 { + req.Header.Add(authenticationapi.ImpersonateUIDHeader, tc.impersonationUid) } - } - if len(tc.impersonationUid) > 0 { - req.Header.Add(authenticationapi.ImpersonateUIDHeader, tc.impersonationUid) - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("%s: unexpected error: %v", tc.name, err) - return - } - if resp.StatusCode != tc.expectedCode { - t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) - return - } + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("%s: unexpected error: %v", tc.name, err) + return + } + if resp.StatusCode != tc.expectedCode { + t.Errorf("%s: expected %v, actual %v", tc.name, tc.expectedCode, resp.StatusCode) + return + } - if !reflect.DeepEqual(actualUser, tc.expectedUser) { - t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) - return - } - }) + if !apiequality.Semantic.DeepEqual(actualUser, tc.expectedUser) { + t.Errorf("%s: expected %#v, actual %#v", tc.name, tc.expectedUser, actualUser) + return + } + }) + } + server.Close() } } diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics.go new file mode 100644 index 000000000..ac47de82a --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics.go @@ -0,0 +1,99 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "sync" + "time" + + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +const ( + namespace = "apiserver" + subsystem = "impersonation" +) + +var ( + impersonationAttemptsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "attempts_total", + Help: "Total number of impersonation attempts split by mode and decision.", + StabilityLevel: metrics.ALPHA, + }, + []string{"mode", "decision"}, + ) + + impersonationAttemptsDurationSeconds = metrics.NewHistogramVec( + &metrics.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "attempts_duration_seconds", + Help: "Latency of impersonation attempts in seconds split by mode and decision.", + StabilityLevel: metrics.ALPHA, + Buckets: metrics.ExponentialBuckets(0.001, 2, 15), + }, + []string{"mode", "decision"}, + ) + + impersonationAuthorizationAttemptsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "authorization_attempts_total", + Help: "Total number of authorization checks made by the impersonation handler split by mode and decision.", + StabilityLevel: metrics.ALPHA, + }, + []string{"mode", "decision"}, + ) + + impersonationAuthorizationAttemptsDurationSeconds = metrics.NewHistogramVec( + &metrics.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "authorization_attempts_duration_seconds", + Help: "Latency of authorization checks made by the impersonation handler in seconds split by mode and decision.", + StabilityLevel: metrics.ALPHA, + Buckets: metrics.ExponentialBuckets(0.001, 2, 15), + }, + []string{"mode", "decision"}, + ) +) + +var registerMetrics sync.Once + +func RegisterMetrics() { + registerMetrics.Do(func() { + legacyregistry.MustRegister(impersonationAttemptsTotal) + legacyregistry.MustRegister(impersonationAttemptsDurationSeconds) + legacyregistry.MustRegister(impersonationAuthorizationAttemptsTotal) + legacyregistry.MustRegister(impersonationAuthorizationAttemptsDurationSeconds) + }) +} + +func RecordImpersonationAttempt(mode, decision string, duration time.Duration) { + impersonationAttemptsTotal.WithLabelValues(mode, decision).Inc() + impersonationAttemptsDurationSeconds.WithLabelValues(mode, decision).Observe(duration.Seconds()) +} + +func RecordImpersonationAuthorizationCall(mode, decision string, duration time.Duration) { + impersonationAuthorizationAttemptsTotal.WithLabelValues(mode, decision).Inc() + impersonationAuthorizationAttemptsDurationSeconds.WithLabelValues(mode, decision).Observe(duration.Seconds()) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics_test.go new file mode 100644 index 000000000..6eb4d0c96 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/metrics/metrics_test.go @@ -0,0 +1,401 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "strings" + "testing" + "time" + + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" +) + +func TestRecordImpersonationAttempt(t *testing.T) { + RegisterMetrics() + + attemptMetrics := []string{ + namespace + "_" + subsystem + "_attempts_total", + namespace + "_" + subsystem + "_attempts_duration_seconds", + } + + testCases := []struct { + name string + mode string + decision string + expectedValue string + }{ + { + name: "allowed with user-info mode", + mode: "user-info", + decision: "allowed", + expectedValue: ` + # HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision. + # TYPE apiserver_impersonation_attempts_total counter + apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1 + # HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision. + # TYPE apiserver_impersonation_attempts_duration_seconds histogram + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1 + apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1 + apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1 + `, + }, + { + name: "denied attempt", + mode: "", + decision: "denied", + expectedValue: ` + # HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision. + # TYPE apiserver_impersonation_attempts_total counter + apiserver_impersonation_attempts_total{decision="denied",mode=""} 1 + # HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision. + # TYPE apiserver_impersonation_attempts_duration_seconds histogram + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.001"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.002"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.004"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.008"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.016"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.032"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.064"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.128"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.256"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.512"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="1.024"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="2.048"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="4.096"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="8.192"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="16.384"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="+Inf"} 1 + apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} 0.1 + apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 1 + `, + }, + { + name: "allowed with legacy mode", + mode: "legacy", + decision: "allowed", + expectedValue: ` + # HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision. + # TYPE apiserver_impersonation_attempts_total counter + apiserver_impersonation_attempts_total{decision="allowed",mode="legacy"} 1 + # HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision. + # TYPE apiserver_impersonation_attempts_duration_seconds histogram + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.001"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.002"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.004"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.008"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.016"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.032"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.064"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.128"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.256"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.512"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="1.024"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="2.048"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="4.096"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="8.192"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="16.384"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="+Inf"} 1 + apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} 0.1 + apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1 + `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resetMetricsForTest() + + RecordImpersonationAttempt(tc.mode, tc.decision, 100*time.Millisecond) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedValue), attemptMetrics...); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestRecordImpersonationAuthorizationCall(t *testing.T) { + RegisterMetrics() + + authorizationMetrics := []string{ + namespace + "_" + subsystem + "_authorization_attempts_total", + namespace + "_" + subsystem + "_authorization_attempts_duration_seconds", + } + + testCases := []struct { + name string + mode string + decision string + expectedValue string + }{ + { + name: "user-info allowed", + mode: "user-info", + decision: "allowed", + expectedValue: ` + # HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_total counter + apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 1 + # HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1 + `, + }, + { + name: "arbitrary-node denied", + mode: "arbitrary-node", + decision: "denied", + expectedValue: ` + # HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_total counter + apiserver_impersonation_authorization_attempts_total{decision="denied",mode="arbitrary-node"} 1 + # HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.064"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.128"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.256"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="0.512"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="1.024"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="2.048"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="4.096"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="8.192"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="16.384"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="arbitrary-node",le="+Inf"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="arbitrary-node"} 0.1 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="arbitrary-node"} 1 + `, + }, + { + name: "legacy allowed", + mode: "legacy", + decision: "allowed", + expectedValue: ` + # HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_total counter + apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="legacy"} 1 + # HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.064"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.128"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.256"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="0.512"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="1.024"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="2.048"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="4.096"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="8.192"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="16.384"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="legacy",le="+Inf"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="legacy"} 0.1 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="legacy"} 1 + `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + resetMetricsForTest() + + RecordImpersonationAuthorizationCall(tc.mode, tc.decision, 100*time.Millisecond) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedValue), authorizationMetrics...); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestRecordImpersonationMetricsMultiple(t *testing.T) { + RegisterMetrics() + resetMetricsForTest() + + RecordImpersonationAttempt("user-info", "allowed", 100*time.Millisecond) + RecordImpersonationAttempt("", "denied", 50*time.Millisecond) + RecordImpersonationAttempt("", "denied", 50*time.Millisecond) + RecordImpersonationAuthorizationCall("user-info", "allowed", 100*time.Millisecond) + RecordImpersonationAuthorizationCall("user-info", "allowed", 100*time.Millisecond) + RecordImpersonationAuthorizationCall("user-info", "denied", 50*time.Millisecond) + RecordImpersonationAuthorizationCall("legacy", "denied", 50*time.Millisecond) + + expectedValue := ` + # HELP apiserver_impersonation_attempts_duration_seconds [ALPHA] Latency of impersonation attempts in seconds split by mode and decision. + # TYPE apiserver_impersonation_attempts_duration_seconds histogram + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 1 + apiserver_impersonation_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.1 + apiserver_impersonation_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 1 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.001"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.002"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.004"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.008"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.016"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.032"} 0 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.064"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.128"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.256"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="0.512"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="1.024"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="2.048"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="4.096"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="8.192"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="16.384"} 2 + apiserver_impersonation_attempts_duration_seconds_bucket{decision="denied",mode="",le="+Inf"} 2 + apiserver_impersonation_attempts_duration_seconds_sum{decision="denied",mode=""} 0.1 + apiserver_impersonation_attempts_duration_seconds_count{decision="denied",mode=""} 2 + # HELP apiserver_impersonation_attempts_total [ALPHA] Total number of impersonation attempts split by mode and decision. + # TYPE apiserver_impersonation_attempts_total counter + apiserver_impersonation_attempts_total{decision="allowed",mode="user-info"} 1 + apiserver_impersonation_attempts_total{decision="denied",mode=""} 2 + # HELP apiserver_impersonation_authorization_attempts_duration_seconds [ALPHA] Latency of authorization checks made by the impersonation handler in seconds split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_duration_seconds histogram + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.064"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.128"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.256"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="0.512"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="1.024"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="2.048"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="4.096"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="8.192"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="16.384"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="allowed",mode="user-info",le="+Inf"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="allowed",mode="user-info"} 0.2 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="allowed",mode="user-info"} 2 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.064"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.128"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.256"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="0.512"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="1.024"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="2.048"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="4.096"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="8.192"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="16.384"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="legacy",le="+Inf"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="legacy"} 0.05 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="legacy"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.001"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.002"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.004"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.008"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.016"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.032"} 0 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.064"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.128"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.256"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="0.512"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="1.024"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="2.048"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="4.096"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="8.192"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="16.384"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_bucket{decision="denied",mode="user-info",le="+Inf"} 1 + apiserver_impersonation_authorization_attempts_duration_seconds_sum{decision="denied",mode="user-info"} 0.05 + apiserver_impersonation_authorization_attempts_duration_seconds_count{decision="denied",mode="user-info"} 1 + # HELP apiserver_impersonation_authorization_attempts_total [ALPHA] Total number of authorization checks made by the impersonation handler split by mode and decision. + # TYPE apiserver_impersonation_authorization_attempts_total counter + apiserver_impersonation_authorization_attempts_total{decision="allowed",mode="user-info"} 2 + apiserver_impersonation_authorization_attempts_total{decision="denied",mode="legacy"} 1 + apiserver_impersonation_authorization_attempts_total{decision="denied",mode="user-info"} 1 + ` + + allMetrics := []string{ + namespace + "_" + subsystem + "_attempts_duration_seconds", + namespace + "_" + subsystem + "_attempts_total", + namespace + "_" + subsystem + "_authorization_attempts_duration_seconds", + namespace + "_" + subsystem + "_authorization_attempts_total", + } + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), allMetrics...); err != nil { + t.Fatal(err) + } +} + +func resetMetricsForTest() { + impersonationAttemptsTotal.Reset() + impersonationAttemptsDurationSeconds.Reset() + impersonationAuthorizationAttemptsTotal.Reset() + impersonationAuthorizationAttemptsDurationSeconds.Reset() +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/mode.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/mode.go new file mode 100644 index 000000000..cdcb02ada --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/impersonation/mode.go @@ -0,0 +1,637 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package impersonation + +import ( + "context" + "fmt" + "slices" + "strings" + + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" +) + +type impersonatedUserInfo struct { + user user.Info + constraint string // the verb used in impersonationModeState.check that allowed this user to be impersonated +} + +// impersonationMode is a type that represents a specific impersonation mode +// it checks if a requester is allowed to make an API request (the attributes) while impersonating a user (the wantedUser) +// a mode may return a cached result if it supports caching (using the input cache key if appropriate) +// a nil impersonatedUserInfo is returned if the mode does not support impersonating the wantedUser +type impersonationMode interface { + check(ctx context.Context, key *impersonationCacheKey, wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (*impersonatedUserInfo, error) + + // all methods below are only used in unit tests + verbForTests() string + cachesForTests() (outer, inner *impersonationCache) +} + +// constrainedImpersonationModeFilter is a function that defines if a specific constrained impersonation mode +// supports the requestor impersonating the wantedUser. It serves as a sudo authorization check for the mode. +type constrainedImpersonationModeFilter func(wantedUser *user.DefaultInfo, requestor user.Info) bool + +func allImpersonationModes(a authorizer.Authorizer) []impersonationMode { + return []impersonationMode{ + associatedNodeImpersonationMode(a), + arbitraryNodeImpersonationMode(a), + serviceAccountImpersonationMode(a), + userInfoImpersonationMode(a), + legacyImpersonationMode(a), + } +} + +// associatedNodeImpersonationMode allows a requestor service account to impersonate the node that it is +// associated with. this is by far the most complex impersonation mode because it caches successful +// impersonation attempts in a way that results in a high cache hit ratio even when the same service account +// is used across different pods running on different nodes (i.e. a node agent running as a daemonset). +// only the username can be specified by the requester. all other fields in user.Info are controlled by the API server. +func associatedNodeImpersonationMode(a authorizer.Authorizer) impersonationMode { + // we wrap the authorizer so that we can override the requestor service account's extra values + // and the node name used in the authorization check. this makes our authorization checks match + // the exact semantics of our cache key which prevents unexpected privilege escalation on a cache + // hit. see the comment below for the cache key details. + wrappedAuthorizer := authorizer.AuthorizerFunc(func(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { + // we use checkAuthorization instead of directly calling the authorizer so we can + // make the error message line up with the actual attributes authorized against + if err := checkAuthorization(ctx, a, &associatedNodeImpersonationAttributes{Attributes: attributes}); err != nil { + return authorizer.DecisionDeny, "", err + } + return authorizer.DecisionAllow, "", nil + }) + mode := newConstrainedImpersonationMode(wrappedAuthorizer, "associated-node", + func(wantedUser *user.DefaultInfo, requestor user.Info) bool { + wantedNodeName := wantedUser.Name + return onlyUsernameSet(wantedUser) && requesterAssociatedWithRequestedNodeUsername(requestor, wantedNodeName) + }, + ) + return &associatedNodeImpersonationCheck{mode: mode} +} + +type associatedNodeImpersonationCheck struct { + mode impersonationMode +} + +func (a *associatedNodeImpersonationCheck) check(ctx context.Context, _ *impersonationCacheKey, wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (*impersonatedUserInfo, error) { + wantedNodeName := wantedUser.Name + // ignore the input cache key because the cache semantics for associated-node require custom logic. + // we know that by the time this key is used, the filter has already verified that the requestor is + // a service account with a node ref that matches the node it is trying to impersonate. + // the actual node being impersonated is not relevant to the cache key, so we just use a static wantedUser. + // we wrap the attributes so that we can drop the requestor service account's extra values. + // this results in the cache key being the same for the same service account across all nodes. + // this is only safe because of the aforementioned filter running before the cache lookup happens. + key := &impersonationCacheKey{wantedUser: &user.DefaultInfo{Name: "system:node:*"}, attributes: &associatedNodeImpersonationAttributes{Attributes: attributes}} + impersonatedNodeWithMaybeIncorrectUsername, err := a.mode.check(ctx, key, wantedUser, attributes) + if err != nil || impersonatedNodeWithMaybeIncorrectUsername == nil { + return nil, err + } + // at this point, we know that we have a successful associated-node impersonation. + // the value could have come from the cache and thus could be for any node, so we wrap the result + // here so that the username matches the node associated with the requestor service account. + return &impersonatedUserInfo{ + user: &associatedNodeImpersonationWantedUserInfo{ + Info: impersonatedNodeWithMaybeIncorrectUsername.user, + name: wantedNodeName, + }, + constraint: impersonatedNodeWithMaybeIncorrectUsername.constraint, + }, nil +} + +func (a *associatedNodeImpersonationCheck) verbForTests() string { + return a.mode.verbForTests() +} + +func (a *associatedNodeImpersonationCheck) cachesForTests() (*impersonationCache, *impersonationCache) { + return a.mode.cachesForTests() +} + +type associatedNodeImpersonationAttributes struct { + authorizer.Attributes +} + +func (a *associatedNodeImpersonationAttributes) GetUser() user.Info { + return &associatedNodeImpersonationRequestorUserInfo{Info: a.Attributes.GetUser()} +} + +func (a *associatedNodeImpersonationAttributes) GetName() string { + if a.GetVerb() == "impersonate:associated-node" { + return "*" // our cache key ignores the node name, so our authorization check needs to be valid for all node names + } + return a.Attributes.GetName() +} + +type associatedNodeImpersonationRequestorUserInfo struct { + user.Info +} + +func (a *associatedNodeImpersonationRequestorUserInfo) GetExtra() map[string][]string { + return map[string][]string{ + // we know the requestor is a service account with a node ref that matches the node it is trying to impersonate + // basically all the extra values would cause cache misses (for example, the node name itself) + // so we drop all the extra values but keep the associated key names + // the authorizer can trust that we have performed the node association check correctly + // the audit log will still contain the full requestor extra fields + // different bound object ref types can result in different extra keys, and thus a different cache key + "authentication.kubernetes.io/associated-node-keys": sets.StringKeySet(a.Info.GetExtra()).List(), + } +} + +type associatedNodeImpersonationWantedUserInfo struct { + user.Info + name string +} + +func (a *associatedNodeImpersonationWantedUserInfo) GetName() string { + return a.name +} + +// arbitraryNodeImpersonationMode implements constrained impersonation for nodes. +// Only the username can be specified by the requester. All other fields in user.Info are controlled by the API server. +func arbitraryNodeImpersonationMode(a authorizer.Authorizer) impersonationMode { + return newConstrainedImpersonationMode(a, "arbitrary-node", + func(wantedUser *user.DefaultInfo, _ user.Info) bool { + if !onlyUsernameSet(wantedUser) { + return false + } + _, ok := isNodeUsername(wantedUser.Name) + return ok + }, + ) +} + +// serviceAccountImpersonationMode implements constrained impersonation for service accounts. +// Only the username can be specified by the requester. All other fields in user.Info are controlled by the API server. +func serviceAccountImpersonationMode(a authorizer.Authorizer) impersonationMode { + return newConstrainedImpersonationMode(a, "serviceaccount", + func(wantedUser *user.DefaultInfo, _ user.Info) bool { + if !onlyUsernameSet(wantedUser) { + return false + } + _, _, ok := isServiceAccountUsername(wantedUser.Name) + return ok + }, + ) +} + +// userInfoImpersonationMode implements constrained impersonation for non-node and non-service account users. +// Unlike the other constrained impersonation modes, it supports impersonating all fields of user.Info. +func userInfoImpersonationMode(a authorizer.Authorizer) impersonationMode { + return newConstrainedImpersonationMode(a, "user-info", + func(wantedUser *user.DefaultInfo, _ user.Info) bool { + // nodes and service accounts cannot be impersonated in this mode. + // the user-info bucket is reserved for the "other" users, that is, + // users that do not have an explicit schema defined by Kube. + if _, ok := isNodeUsername(wantedUser.Name); ok { + return false + } + if _, _, ok := isServiceAccountUsername(wantedUser.Name); ok { + return false + } + return true + }, + ) +} + +// legacyImpersonationMode is a complete reimplementation of the original impersonation mode that has +// existed in kube since v1.3. The behavior is expected to be identical to the original implementation. +func legacyImpersonationMode(a authorizer.Authorizer) impersonationMode { + return &legacyImpersonationCheck{m: newImpersonationModeState(a, "impersonate", false)} +} + +type legacyImpersonationCheck struct { + m *impersonationModeState +} + +func (l *legacyImpersonationCheck) check(ctx context.Context, key *impersonationCacheKey, wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (*impersonatedUserInfo, error) { + requestor := attributes.GetUser() + return l.m.check(ctx, key, wantedUser, requestor) +} + +func (l *legacyImpersonationCheck) verbForTests() string { + return l.m.verb +} + +func (l *legacyImpersonationCheck) cachesForTests() (*impersonationCache, *impersonationCache) { + // legacy impersonation has no outer layer so just return an empty cache + // though an inner cache is present, it is unused + return newImpersonationCache(false), l.m.cache +} + +func newConstrainedImpersonationMode(a authorizer.Authorizer, mode string, filter constrainedImpersonationModeFilter) impersonationMode { + return &constrainedImpersonationModeState{ + state: newImpersonationModeState(a, "impersonate:"+mode, true), + cache: newImpersonationCache(false), + authorizer: a, + mode: mode, + filter: filter, + } +} + +// constrainedImpersonationModeState implements the secondary authorization check via impersonate-on:: to +// determine if the requestor is authorized to perform the specific verb when impersonating the wantedUser via mode. +// if this check succeeds, the primary authorization checks are run, see impersonationModeState for details. +// if the mode's filter does not match the inputs, the impersonation automatically fails and returns a nil impersonatedUserInfo. +type constrainedImpersonationModeState struct { + state *impersonationModeState + // this outer cache covers the overall impersonation for this mode, i.e. a cache hit here short-circuits all checks + // skipAttributes is false, i.e. this cache depends on the request being made, not just the user being impersonated by the requestor + // it is expected to have a low hit ratio because the requestor is unlikely to make the same request multiple times in a short period + cache *impersonationCache + authorizer authorizer.Authorizer + mode string + filter constrainedImpersonationModeFilter +} + +func (c *constrainedImpersonationModeState) check(ctx context.Context, key *impersonationCacheKey, wantedUser *user.DefaultInfo, attributes authorizer.Attributes) (*impersonatedUserInfo, error) { + requestor := attributes.GetUser() + // we must call the filter before doing anything because this serves as a sudo authorization check to say "does this mode even apply?" + // also the cache key is not always a direct match with wantedUser+attributes, so again, we must call the filter first + if !c.filter(wantedUser, requestor) { + return nil, nil + } + + if impersonatedUser := c.cache.get(key); impersonatedUser != nil { + return impersonatedUser, nil + } + + if err := checkAuthorization(ctx, c.authorizer, &impersonateOnAttributes{mode: c.mode, Attributes: attributes}); err != nil { + return nil, err + } + + impersonatedUser, err := c.state.check(ctx, key, wantedUser, requestor) + if err != nil || impersonatedUser == nil { + return nil, err + } + c.cache.set(key, impersonatedUser) + return impersonatedUser, nil +} + +func (c *constrainedImpersonationModeState) verbForTests() string { + return c.state.verb +} + +func (c *constrainedImpersonationModeState) cachesForTests() (*impersonationCache, *impersonationCache) { + return c.cache, c.state.cache +} + +// impersonationModeState implements the primary authorization checks via the impersonate: verb for constrained +// impersonation and the impersonate verb for legacy impersonation. each field that is set in the wantedUser +// results in one or more authorization checks to determine if the requestor has access to impersonate that value. +type impersonationModeState struct { + authorizer authorizer.Authorizer + verb string + isConstrainedImpersonation bool + + usernameAndGroupGV schema.GroupVersion + constraint string + + // this inner cache covers the checks related to the specific fields set in wantedUser + // skipAttributes is true, i.e. this cache only depends on the user being impersonated by the requestor + // it is expected to have a high hit ratio because the requestor may impersonate the same user for many different requests + cache *impersonationCache +} + +func newImpersonationModeState(a authorizer.Authorizer, verb string, isConstrainedImpersonation bool) *impersonationModeState { + usernameAndGroupGV := authenticationv1.SchemeGroupVersion + constraint := verb + if !isConstrainedImpersonation { + usernameAndGroupGV = corev1.SchemeGroupVersion + constraint = "" + } + return &impersonationModeState{ + authorizer: a, + verb: verb, + isConstrainedImpersonation: isConstrainedImpersonation, + + usernameAndGroupGV: usernameAndGroupGV, + constraint: constraint, + cache: newImpersonationCache(true), + } +} + +func (m *impersonationModeState) check(ctx context.Context, key *impersonationCacheKey, wantedUser *user.DefaultInfo, requestor user.Info) (*impersonatedUserInfo, error) { + // we only use caching in constrained impersonation mode to avoid any behavioral changes with legacy impersonation + if m.isConstrainedImpersonation { + if impersonatedUser := m.cache.get(key); impersonatedUser != nil { + return impersonatedUser, nil + } + } + + actualUser := *wantedUser + + if err := m.authorizeUsername(ctx, requestor, wantedUser.Name, wantedUser.Groups, &actualUser); err != nil { + return nil, err + } + + if err := m.authorizeUID(ctx, requestor, wantedUser.UID); err != nil { + return nil, err + } + + if err := m.authorizeGroups(ctx, requestor, wantedUser.Groups); err != nil { + return nil, err + } + + if err := m.authorizeExtra(ctx, requestor, wantedUser.Extra); err != nil { + return nil, err + } + + if actualUser.Name == user.Anonymous { + ensureGroup(&actualUser, user.AllUnauthenticated) + } else if !slices.Contains(actualUser.Groups, user.AllUnauthenticated) { + ensureGroup(&actualUser, user.AllAuthenticated) + } + + impersonatedUser := &impersonatedUserInfo{user: &actualUser, constraint: m.constraint} + if m.isConstrainedImpersonation { + m.cache.set(key, impersonatedUser) + } + return impersonatedUser, nil +} + +func (m *impersonationModeState) authorizeUsername(ctx context.Context, requestor user.Info, username string, wantedUserGroups []string, actualUser *user.DefaultInfo) error { + usernameAttributes := impersonationAttributes(requestor, m.usernameAndGroupGV, m.verb, "users", username) + + if m.isConstrainedImpersonation { + if name, ok := isNodeUsername(username); ok { + usernameAttributes.Resource = "nodes" + usernameAttributes.Name = name + + // this should be impossible to reach but check just in case + if len(wantedUserGroups) != 0 { + return responsewriters.ForbiddenStatusError(usernameAttributes, fmt.Sprintf("when impersonating a node, cannot impersonate groups %q", wantedUserGroups)) + } + + actualUser.Groups = []string{user.NodesGroup} // all nodes have a fixed group list in constrained impersonation + } + } + + if namespace, name, ok := isServiceAccountUsername(username); ok { + usernameAttributes.Resource = "serviceaccounts" + usernameAttributes.Namespace = namespace + usernameAttributes.Name = name + + // this should be impossible to reach but check just in case + if m.isConstrainedImpersonation && len(wantedUserGroups) != 0 { + return responsewriters.ForbiddenStatusError(usernameAttributes, fmt.Sprintf("when impersonating a service account, cannot impersonate groups %q", wantedUserGroups)) + } + + if len(wantedUserGroups) == 0 { + // if groups are not specified for a service account, we know the groups because it is a fixed mapping. Add them + actualUser.Groups = serviceaccount.MakeGroupNames(namespace) + } + } + + return checkAuthorization(ctx, m.authorizer, usernameAttributes) +} + +func (m *impersonationModeState) authorizeUID(ctx context.Context, requestor user.Info, uid string) error { + if len(uid) == 0 { + return nil + } + uidAttributes := impersonationAttributes(requestor, authenticationv1.SchemeGroupVersion, m.verb, "uids", uid) + return checkAuthorization(ctx, m.authorizer, uidAttributes) +} + +// manyAuthorizationChecksInLoop is an arbitrary value used in constrained impersonation modes to decide if they +// should try to perform a single wildcard authorization check before making many individual checks in a loop. +const manyAuthorizationChecksInLoop = 4 + +func (m *impersonationModeState) authorizeGroups(ctx context.Context, requestor user.Info, groups []string) error { + if len(groups) == 0 { + return nil + } + + groupAttributes := impersonationAttributes(requestor, m.usernameAndGroupGV, m.verb, "groups", "") + + // perform extra sanity checks that would be backwards incompatible with legacy impersonation + if m.isConstrainedImpersonation { + if slices.Contains(groups, "") { + return responsewriters.ForbiddenStatusError(groupAttributes, "impersonating the empty string group is not allowed") + } + if slices.Contains(groups, user.SystemPrivilegedGroup) { + groupAttributes.Name = user.SystemPrivilegedGroup + return responsewriters.ForbiddenStatusError(groupAttributes, "impersonating the system:masters group is not allowed") + } + } + + // if the requestor is trying to impersonate many groups at once, see if they are authorized to impersonate all groups + // we only do this in constrained impersonation mode to avoid any behavioral changes with legacy impersonation + if m.isConstrainedImpersonation && len(groups) >= manyAuthorizationChecksInLoop { + groupAttributes.Name = "*" + if err := checkAuthorization(ctx, m.authorizer, groupAttributes); err == nil { + return nil + } + } + + for _, group := range groups { + groupAttributes.Name = group + if err := checkAuthorization(ctx, m.authorizer, groupAttributes); err != nil { + return err + } + } + + return nil +} + +func (m *impersonationModeState) authorizeExtra(ctx context.Context, requestor user.Info, extra map[string][]string) error { + if len(extra) == 0 { + return nil + } + + extraAttributes := impersonationAttributes(requestor, authenticationv1.SchemeGroupVersion, m.verb, "userextras", "") + + // perform extra sanity checks that would be backwards incompatible with legacy impersonation + if m.isConstrainedImpersonation { + if err := validateExtra(extra); err != nil { + return responsewriters.ForbiddenStatusError(extraAttributes, err.Error()) + } + } + + // if the requestor is trying to impersonate many extras at once, see if they are authorized to impersonate all extras + // we only do this in constrained impersonation mode to avoid any behavioral changes with legacy impersonation + if m.isConstrainedImpersonation && isLargeExtra(extra) { + extraAttributes.Subresource = "*" + extraAttributes.Name = "*" + if err := checkAuthorization(ctx, m.authorizer, extraAttributes); err == nil { + return nil + } + } + + for key, values := range extra { + extraAttributes.Subresource = key + for _, value := range values { + extraAttributes.Name = value + if err := checkAuthorization(ctx, m.authorizer, extraAttributes); err != nil { + return err + } + } + } + + return nil +} + +func validateExtra(extra map[string][]string) error { + fp := field.NewPath("extra", "key") + for key, values := range extra { + if len(key) == 0 { + return fmt.Errorf("impersonating the empty string key in extra is not allowed") + } + if err := utilvalidation.IsDomainPrefixedPath(fp, key).ToAggregate(); err != nil { + return fmt.Errorf("impersonating an invalid key in extra is not allowed: %w", err) + } + if key != strings.ToLower(key) { + return fmt.Errorf("impersonating a non-lowercase key in extra is not allowed: %q", key) + } + if len(values) == 0 { + return fmt.Errorf("impersonating empty values in extra is not allowed") + } + if slices.Contains(values, "") { + return fmt.Errorf("impersonating the empty string value in extra is not allowed") + } + } + return nil +} + +func isLargeExtra(extra map[string][]string) bool { + if len(extra) >= manyAuthorizationChecksInLoop { + return true + } + var count int + for _, values := range extra { + count += len(values) + if count >= manyAuthorizationChecksInLoop { + return true + } + } + return false +} + +func impersonationAttributes(requestor user.Info, gv schema.GroupVersion, verb, resource, name string) authorizer.AttributesRecord { + return authorizer.AttributesRecord{ + User: requestor, + Verb: verb, + APIGroup: gv.Group, + APIVersion: gv.Version, + Resource: resource, + Name: name, + ResourceRequest: true, + } +} + +// impersonateOnAttributes is a simple wrapper that updates the verb of the attributes to impersonate-on:: +// This allows the expression of "a subject can perform this verb while using this impersonation mode" +type impersonateOnAttributes struct { + mode string + authorizer.Attributes +} + +func (i *impersonateOnAttributes) GetVerb() string { + return "impersonate-on:" + i.mode + ":" + i.Attributes.GetVerb() +} + +func checkAuthorization(ctx context.Context, a authorizer.Authorizer, attributes authorizer.Attributes) error { + authorized, reason, err := a.Authorize(ctx, attributes) + + // an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here. + if authorized == authorizer.DecisionAllow { + return nil + } + + // if the authorizer gave us a forbidden error, do not wrap it again + if errors.IsForbidden(err) { + return err + } + + msg := reason + switch { + case err != nil && len(reason) > 0: + msg = fmt.Sprintf("%v: %s", err, reason) + case err != nil: + msg = err.Error() + } + + return responsewriters.ForbiddenStatusError(attributes, msg) +} + +func ensureGroup(u *user.DefaultInfo, group string) { + if slices.Contains(u.Groups, group) { + return + } + + // do not mutate a slice that we did not create + groups := make([]string, 0, len(u.Groups)+1) + groups = append(groups, u.Groups...) + groups = append(groups, group) + u.Groups = groups +} + +func isServiceAccountUsername(username string) (namespace, name string, ok bool) { + namespace, name, err := serviceaccount.SplitUsername(username) + return namespace, name, err == nil +} + +// matches the real ValidateNodeName from k8s.io/kubernetes/pkg/apis/core/validation +// which we are not allowed to import here +var validateNodeName = validation.NameIsDNSSubdomain + +func isNodeUsername(username string) (string, bool) { + const nodeUsernamePrefix = "system:node:" + if !strings.HasPrefix(username, nodeUsernamePrefix) { + return "", false + } + name := strings.TrimPrefix(username, nodeUsernamePrefix) + if len(validateNodeName(name, false)) != 0 { + return "", false + } + return name, true +} + +func requesterAssociatedWithRequestedNodeUsername(requestor user.Info, username string) bool { + nodeName, ok := isNodeUsername(username) + if !ok { + return false + } + if _, _, ok := isServiceAccountUsername(requestor.GetName()); !ok { + return false + } + return getExtraValue(requestor, serviceaccount.NodeNameKey) == nodeName +} + +func getExtraValue(u user.Info, key string) string { + values := u.GetExtra()[key] + if len(values) != 1 { + return "" + } + return values[0] +} + +func onlyUsernameSet(u user.Info) bool { + return len(u.GetUID()) == 0 && len(u.GetGroups()) == 0 && len(u.GetExtra()) == 0 +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/mux_discovery_complete.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/mux_discovery_complete.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/mux_discovery_complete.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/mux_discovery_complete.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/mux_discovery_complete_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/mux_discovery_complete_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/mux_discovery_complete_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/mux_discovery_complete_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/read_write_deadline_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/read_write_deadline_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/read_write_deadline_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/read_write_deadline_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_deadline.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_deadline.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_deadline.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_deadline.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_deadline_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_deadline_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_deadline_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_deadline_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_received_time.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_received_time.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_received_time.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_received_time.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_received_time_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_received_time_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/request_received_time_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/request_received_time_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/requestinfo.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/requestinfo.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/requestinfo.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/requestinfo.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/requestinfo_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/requestinfo_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/requestinfo_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/requestinfo_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/storageversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/storageversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/storageversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/storageversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/traces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/traces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/traces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/traces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/warning.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/warning.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/warning.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/warning.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/warning_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/warning_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/warning_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/warning_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/webhook_duration.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/webhook_duration.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/filters/webhook_duration.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/filters/webhook_duration.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/groupversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/groupversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/groupversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/groupversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/create.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/create.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/create.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/create.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/create_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/create_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/create_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/create_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete.go index e8b74c8a9..3d0800747 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/delete.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete.go @@ -103,11 +103,13 @@ func DeleteResource(r rest.GracefulDeleter, allowsOptions bool, scope *RequestSc defaultGVK := scope.MetaGroupVersion.WithKind("DeleteOptions") obj, gvk, err := apihelpers.GetMetaInternalVersionCodecs().DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { + err = errors.NewBadRequest(err.Error()) scope.err(err, w, req) return } if obj != options { - scope.err(fmt.Errorf("decoded object cannot be converted to DeleteOptions"), w, req) + err = errors.NewBadRequest("decoded object cannot be converted to DeleteOptions") + scope.err(err, w, req) return } span.AddEvent("Decoded delete options") @@ -278,11 +280,13 @@ func DeleteCollection(r rest.CollectionDeleter, checkBody bool, scope *RequestSc defaultGVK := scope.MetaGroupVersion.WithKind("DeleteOptions") obj, gvk, err := apihelpers.GetMetaInternalVersionCodecs().DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(body, &defaultGVK, options) if err != nil { + err = errors.NewBadRequest(err.Error()) scope.err(err, w, req) return } if obj != options { - scope.err(fmt.Errorf("decoded object cannot be converted to DeleteOptions"), w, req) + err = errors.NewBadRequest("decoded object cannot be converted to DeleteOptions") + scope.err(err, w, req) return } @@ -411,7 +415,7 @@ func authorizeUnsafeDelete(ctx context.Context, attr admission.Attributes, authz decision, reason, err := authz.Authorize(ctx, record) if err != nil { err = fmt.Errorf("error while checking permission for %q, %w", record.Verb, err) - klog.FromContext(ctx).V(1).Error(err, "failed to authorize") + klog.FromContext(ctx).V(1).Info("failed to authorize", "err", err) return admission.NewForbidden(attr, err) } if decision == authorizer.DecisionAllow { diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete_test.go new file mode 100644 index 000000000..d0d50fcd0 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/delete_test.go @@ -0,0 +1,1164 @@ +/* +Copyright 2019 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 handlers + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "sync/atomic" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apiserver/pkg/admission" + auditinternal "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/registry/rest" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + + "k8s.io/utils/ptr" + + "github.com/google/go-cmp/cmp" +) + +type mockCodecs struct { + serializer.CodecFactory + err error +} + +type mockCodec struct { + runtime.Codec + codecs *mockCodecs +} + +func (p mockCodec) Encode(obj runtime.Object, w io.Writer) error { + err := p.Codec.Encode(obj, w) + p.codecs.err = err + return err +} + +func (s *mockCodecs) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + out := s.CodecFactory.CodecForVersions(encoder, nil, gv, nil) + return &mockCodec{ + Codec: out, + codecs: s, + } +} + +func TestDeleteResourceAuditLogRequestObject(t *testing.T) { + + ctx := audit.WithAuditContext(context.TODO()) + ac := audit.AuditContextFrom(ctx) + if err := ac.Init(audit.RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, nil); err != nil { + t.Fatal(err) + } + + policy := metav1.DeletePropagationBackground + deleteOption := &metav1.DeleteOptions{ + GracePeriodSeconds: ptr.To[int64](30), + PropagationPolicy: &policy, + } + + fakeCorev1GroupVersion := schema.GroupVersion{ + Group: "", + Version: "v1", + } + testScheme := runtime.NewScheme() + metav1.AddToGroupVersion(testScheme, fakeCorev1GroupVersion) + testCodec := serializer.NewCodecFactory(testScheme) + + tests := []struct { + name string + object runtime.Object + gv schema.GroupVersion + serializer serializer.CodecFactory + ok bool + }{ + { + name: "meta built-in Codec encode v1.DeleteOptions", + object: &metav1.DeleteOptions{ + GracePeriodSeconds: ptr.To[int64](30), + PropagationPolicy: &policy, + }, + gv: metav1.SchemeGroupVersion, + serializer: metainternalversionscheme.Codecs, + ok: true, + }, + { + name: "fake corev1 registered codec encode v1 DeleteOptions", + object: &metav1.DeleteOptions{ + GracePeriodSeconds: ptr.To[int64](30), + PropagationPolicy: &policy, + }, + gv: metav1.SchemeGroupVersion, + serializer: testCodec, + ok: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + codecs := &mockCodecs{} + codecs.CodecFactory = test.serializer + + audit.LogRequestObject(ctx, deleteOption, test.gv, schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "pods", + }, "", codecs) + + err := codecs.err + if err != nil { + if test.ok { + t.Errorf("expect nil but got %#v", err) + } + t.Logf("encode object: %#v", err) + } else { + if !test.ok { + t.Errorf("expect err but got nil") + } + } + }) + } +} + +// For issue https://github.com/kubernetes/kubernetes/issues/132359 +func TestDeleteResourceDeleteOptions(t *testing.T) { + ctx := t.Context() + fakeDeleterFn := func(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) { + return nil, false, nil + } + js := jsonserializer.NewSerializerWithOptions(jsonserializer.DefaultMetaFactory, nil, nil, jsonserializer.SerializerOptions{}) + scope := &RequestScope{ + Namer: &mockNamer{}, + Serializer: &fakeSerializer{ + serializer: runtime.NewCodec(js, js), + }, + } + handler := DeleteResource(fakeDeleterFunc(fakeDeleterFn), true, scope, nil) + + tests := []struct { + name string + body string + expectCode int + }{ + { + name: "valid delete options", + body: "{}", + expectCode: 200, + }, + { + name: "orphanDependents invalid Go type", + body: `{"orphanDependents": "randomString"}`, + expectCode: 400, + }, + { + name: "apiVersion/kind mismatch", + body: `{ "apiVersion": "v1", "kind": "APIResourceList" }`, + expectCode: 400, + }, + { + name: "apiVersion invalid type", + body: `{ "apiVersion": false, "kind": "DeleteOptions" }`, + expectCode: 400, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req, err := http.NewRequestWithContext(ctx, request.MethodDelete, "/api/v1/namespaces/default/pods/testpod", strings.NewReader(test.body)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "*/*") + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + gotCode := recorder.Code + if gotCode != test.expectCode { + t.Fatalf("expected status %v but got %v", test.expectCode, gotCode) + } + }) + } +} + +type fakeDeleterFunc func(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) + +func (f fakeDeleterFunc) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + return f(ctx, deleteValidation, options) +} + +func TestDeleteCollection(t *testing.T) { + req := &http.Request{ + Header: http.Header{}, + } + req.Header.Set("Content-Type", "application/json") + + fakeCorev1GroupVersion := schema.GroupVersion{ + Group: "", + Version: "v1", + } + fakeCorev1Scheme := runtime.NewScheme() + fakeCorev1Scheme.AddKnownTypes(fakeCorev1GroupVersion, &metav1.DeleteOptions{}) + fakeCorev1Codec := serializer.NewCodecFactory(fakeCorev1Scheme) + + tests := []struct { + name string + codecFactory serializer.CodecFactory + data []byte + expectErr string + }{ + // for issue: https://github.com/kubernetes/kubernetes/issues/111985 + { + name: "decode '{}' to metav1.DeleteOptions with fakeCorev1Codecs", + codecFactory: fakeCorev1Codec, + data: []byte("{}"), + expectErr: "no kind \"DeleteOptions\" is registered", + }, + { + name: "decode '{}' to metav1.DeleteOptions with metainternalversionscheme.Codecs", + codecFactory: metainternalversionscheme.Codecs, + data: []byte("{}"), + expectErr: "", + }, + { + name: "decode versioned (corev1) DeleteOptions with metainternalversionscheme.Codecs", + codecFactory: metainternalversionscheme.Codecs, + data: []byte(`{"apiVersion":"v1","kind":"DeleteOptions","gracePeriodSeconds":123}`), + expectErr: "", + }, + { + name: "decode versioned (foo) DeleteOptions with metainternalversionscheme.Codecs", + codecFactory: metainternalversionscheme.Codecs, + data: []byte(`{"apiVersion":"foo/v1","kind":"DeleteOptions","gracePeriodSeconds":123}`), + expectErr: "", + }, + } + + defaultGVK := metav1.SchemeGroupVersion.WithKind("DeleteOptions") + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s, err := negotiation.NegotiateInputSerializer(req, false, test.codecFactory) + if err != nil { + t.Fatal(err) + } + + options := &metav1.DeleteOptions{} + _, _, err = metainternalversionscheme.Codecs.DecoderToVersion(s.Serializer, defaultGVK.GroupVersion()).Decode(test.data, &defaultGVK, options) + if test.expectErr != "" { + if err == nil { + t.Fatalf("expect %s but got nil", test.expectErr) + } + if !strings.Contains(err.Error(), test.expectErr) { + t.Fatalf("expect %s but got %s", test.expectErr, err.Error()) + } + } + }) + } +} + +// For issue https://github.com/kubernetes/kubernetes/issues/132359 +func TestDeleteCollectionDeleteOptions(t *testing.T) { + ctx := t.Context() + fakeDeleterFn := func(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *metainternalversion.ListOptions) (runtime.Object, error) { + return nil, nil + } + js := jsonserializer.NewSerializerWithOptions(jsonserializer.DefaultMetaFactory, nil, nil, jsonserializer.SerializerOptions{}) + scope := &RequestScope{ + Namer: &mockNamer{}, + Serializer: &fakeSerializer{ + serializer: runtime.NewCodec(js, js), + }, + } + handler := DeleteCollection(fakeCollectionDeleterFunc(fakeDeleterFn), true, scope, nil) + + tests := []struct { + name string + body string + expectCode int + }{ + { + name: "valid delete options", + body: "{}", + expectCode: 200, + }, + { + name: "orphanDependents invalid Go type", + body: `{"orphanDependents": "randomString"}`, + expectCode: 400, + }, + { + name: "apiVersion/kind mismatch", + body: `{ "apiVersion": "v1", "kind": "APIResourceList" }`, + expectCode: 400, + }, + { + name: "apiVersion invalid type", + body: `{ "apiVersion": false, "kind": "DeleteOptions" }`, + expectCode: 400, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req, err := http.NewRequestWithContext(ctx, request.MethodDelete, "/api/v1/namespaces/default/pods/testpod", strings.NewReader(test.body)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "*/*") + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + gotCode := recorder.Code + if gotCode != test.expectCode { + t.Fatalf("expected status %v but got %v", test.expectCode, gotCode) + } + }) + } +} + +func TestDeleteCollectionWithNoContextDeadlineEnforced(t *testing.T) { + ctx := t.Context() + var invokedGot, hasDeadlineGot int32 + fakeDeleterFn := func(ctx context.Context, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions, _ *metainternalversion.ListOptions) (runtime.Object, error) { + // we expect CollectionDeleter to be executed once + atomic.AddInt32(&invokedGot, 1) + + // we don't expect any context deadline to be set + if _, hasDeadline := ctx.Deadline(); hasDeadline { + atomic.AddInt32(&hasDeadlineGot, 1) + } + return nil, nil + } + + // do the minimum setup to ensure that it gets as far as CollectionDeleter + scope := &RequestScope{ + Namer: &mockNamer{}, + Serializer: &fakeSerializer{ + serializer: runtime.NewCodec(runtime.NoopEncoder{}, runtime.NoopDecoder{}), + }, + } + handler := DeleteCollection(fakeCollectionDeleterFunc(fakeDeleterFn), false, scope, nil) + + request, err := http.NewRequestWithContext(ctx, request.MethodGet, "/test", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // the request context should not have any deadline by default + if _, hasDeadline := request.Context().Deadline(); hasDeadline { + t.Fatalf("expected request context to not have any deadline") + } + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + if atomic.LoadInt32(&invokedGot) != 1 { + t.Errorf("expected collection deleter to be invoked") + } + if atomic.LoadInt32(&hasDeadlineGot) > 0 { + t.Errorf("expected context to not have any deadline") + } +} + +type fakeCollectionDeleterFunc func(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) + +func (f fakeCollectionDeleterFunc) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *metainternalversion.ListOptions) (runtime.Object, error) { + return f(ctx, deleteValidation, options, listOptions) +} + +type fakeSerializer struct { + serializer runtime.Serializer +} + +func (n *fakeSerializer) SupportedMediaTypes() []runtime.SerializerInfo { + return []runtime.SerializerInfo{ + { + MediaType: "application/json", + MediaTypeType: "application", + MediaTypeSubType: "json", + }, + } +} +func (n *fakeSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder { + return n.serializer +} +func (n *fakeSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { + return n.serializer +} + +func TestAuthorizeUnsafeDelete(t *testing.T) { + const verbWant = "unsafe-delete-ignore-read-errors" + tests := []struct { + name string + reqInfo *request.RequestInfo + attr admission.Attributes + authz authorizer.Authorizer + err func(admission.Attributes) error + }{ + { + name: "operation is not delete, admit", + attr: newAttributes(attributes{operation: admission.Update}), + authz: nil, // Authorize should not be invoked + }, + { + name: "feature enabled, delete, operation option is nil, admit", + attr: newAttributes(attributes{ + operation: admission.Delete, + operationOptions: nil, + }), + authz: nil, // Authorize should not be invoked + }, + { + name: "delete, operation option is not a match, forbid", + attr: newAttributes(attributes{ + operation: admission.Delete, + operationOptions: &metav1.PatchOptions{}, + }), + authz: nil, // Authorize should not be invoked + err: func(admission.Attributes) error { + return errors.NewInternalError(fmt.Errorf("expected an option of type: %T, but got: %T", &metav1.DeleteOptions{}, &metav1.PatchOptions{})) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is nil, admit", + attr: newAttributes(attributes{ + operation: admission.Delete, + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: nil, + }, + }), + authz: nil, // Authorize should not be invoked + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is false, admit", + attr: newAttributes(attributes{ + operation: admission.Delete, + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](false), + }, + }), + authz: nil, // Authorize should not be invoked + }, + { + name: "feature enabled, delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, no RequestInfo in request context, forbid", + reqInfo: nil, + attr: newAttributes(attributes{ + operation: admission.Delete, + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: nil, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("no RequestInfo found in the context")) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, subresource request, forbid", + reqInfo: &request.RequestInfo{IsResourceRequest: true}, + attr: newAttributes(attributes{ + operation: admission.Delete, + subresource: "foo", + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: nil, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("ignoreStoreReadErrorWithClusterBreakingPotential delete option is not allowed on a subresource or non-resource request")) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, subresource request, forbid", + reqInfo: &request.RequestInfo{IsResourceRequest: false}, + attr: newAttributes(attributes{ + operation: admission.Delete, + subresource: "", + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: nil, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("ignoreStoreReadErrorWithClusterBreakingPotential delete option is not allowed on a subresource or non-resource request")) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, authorizer returns error, forbid", + reqInfo: &request.RequestInfo{IsResourceRequest: true}, + attr: newAttributes(attributes{ + subresource: "", + operation: admission.Delete, + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: &fakeAuthorizer{err: fmt.Errorf("unexpected error")}, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("error while checking permission for %q, %w", verbWant, fmt.Errorf("unexpected error"))) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, user does not have permission, forbid", + reqInfo: &request.RequestInfo{IsResourceRequest: true}, + attr: newAttributes(attributes{ + operation: admission.Delete, + subresource: "", + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: &fakeAuthorizer{ + decision: authorizer.DecisionDeny, + reason: "does not have permission", + }, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("not permitted to do %q, reason: %s", verbWant, "does not have permission")) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, authorizer gives no opinion, forbid", + reqInfo: &request.RequestInfo{IsResourceRequest: true}, + attr: newAttributes(attributes{ + operation: admission.Delete, + subresource: "", + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + }), + authz: &fakeAuthorizer{ + decision: authorizer.DecisionNoOpinion, + reason: "no opinion", + }, + err: func(attr admission.Attributes) error { + return admission.NewForbidden(attr, fmt.Errorf("not permitted to do %q, reason: %s", verbWant, "no opinion")) + }, + }, + { + name: "delete, IgnoreStoreReadErrorWithClusterBreakingPotential is true, user has permission, admit", + reqInfo: &request.RequestInfo{IsResourceRequest: true}, + attr: newAttributes(attributes{ + operation: admission.Delete, + subresource: "", + operationOptions: &metav1.DeleteOptions{ + IgnoreStoreReadErrorWithClusterBreakingPotential: ptr.To[bool](true), + }, + userInfo: &user.DefaultInfo{Name: "foo"}, + }), + authz: &fakeAuthorizer{ + decision: authorizer.DecisionAllow, + reason: "permitted", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var want error + if test.err != nil { + want = test.err(test.attr) + } + + ctx := context.Background() + if test.reqInfo != nil { + ctx = request.WithRequestInfo(ctx, test.reqInfo) + } + + // wrap the attributes so we can access the annotations set during admission + attrs := &fakeAttributes{Attributes: test.attr} + got := authorizeUnsafeDelete(ctx, attrs, test.authz) + switch { + case want != nil: + if got == nil || want.Error() != got.Error() { + t.Errorf("expected error: %v, but got: %v", want, got) + } + default: + if got != nil { + t.Errorf("expected no error, but got: %v", got) + } + } + }) + } +} + +// attributes of interest for this test +type attributes struct { + operation admission.Operation + operationOptions runtime.Object + userInfo user.Info + subresource string +} + +func newAttributes(attr attributes) admission.Attributes { + return admission.NewAttributesRecord( + nil, // this plugin should never inspect the object + nil, // old object, this plugin should never inspect it + schema.GroupVersionKind{}, // this plugin should never inspect kind + "", // namespace, leave it empty, this plugin only passes it along to the authorizer + "", // name, leave it empty, this plugin only passes it along to the authorizer + schema.GroupVersionResource{}, // resource, leave it empty, this plugin only passes it along to the authorizer + attr.subresource, + attr.operation, + attr.operationOptions, + false, // dryRun, this plugin should never inspect this attribute + attr.userInfo) +} + +type fakeAttributes struct { + admission.Attributes + annotations map[string]string +} + +func (f *fakeAttributes) AddAnnotation(key, value string) error { + if err := f.Attributes.AddAnnotation(key, value); err != nil { + return err + } + + if len(f.annotations) == 0 { + f.annotations = map[string]string{} + } + f.annotations[key] = value + return nil +} + +type fakeAuthorizer struct { + decision authorizer.Decision + reason string + err error +} + +func (authorizer fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + return authorizer.decision, authorizer.reason, authorizer.err +} + +func TestDeleteResourceWithUnsafeDeletionFlow(t *testing.T) { + success := metav1.Status{Status: "Success", Code: 200, Details: &metav1.StatusDetails{}} + newInternalError := func(err error) metav1.Status { + status := errors.NewInternalError(err).ErrStatus + status.Kind = "Status" + status.APIVersion = "v1" + return status + } + newForbiddenError := func() metav1.Status { + status := errors.NewForbidden(schema.GroupResource{}, "Unknown", + fmt.Errorf("not permitted to do %q, reason: %s", "unsafe-delete-ignore-read-errors", "not permitted")).ErrStatus + status.Kind = "Status" + status.APIVersion = "v1" + return status + } + + tests := []struct { + name string + featureEnabled bool + // whether the registry implements rest.CorruptObjectDeleterProvider, and provides an unsafe deleter: + // nil: it does not implement the rest.CorruptObjectDeleterProvider interface + // true: it implements CorruptObjectDeleterProvider, and returns a valid unsafe deleter + // false: it implements CorruptObjectDeleterProvider, but returns nil when asked for an unsafe deleter + registryHasUnsafeDeleter *bool + // what the user passes in the delete options for ignoreStoreReadErrorWithClusterBreakingPotential + ignoreReadErr *bool + authorizer authorizer.Authorizer + // want + normalFlowWant *deletionFlowTracker // what the normal deletion flow should observe + unsafeFlowWant *deletionFlowTracker // what the unsafe deletion flow should observe + unsafeAnnotationWant bool // whether the unsafe audit annotation should be added + status metav1.Status // status we expect from the HTTP response + }{ + { + name: "feature disabled, registry does not implement CorruptObjectDeleterProvider, ignore is false, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: nil, + ignoreReadErr: ptr.To(false), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature disabled, registry does not implement CorruptObjectDeleterProvider, ignore is true, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: nil, + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature disabled, registry provides a nil unsafe deleter, ignore is false, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: ptr.To(false), + ignoreReadErr: ptr.To(false), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature disabled, registry provides a nil unsafe deleter, ignore is true, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: ptr.To(false), + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature disabled, registry provides a valid unsafe deleter, ignore is false, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(false), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature disabled, registry provides a valid unsafe deleter, ignore is true, should invoke the normal deletion flow", + featureEnabled: false, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + + // feature enabled + { + name: "feature enabled, registry does not implement CorruptObjectDeleterProvider, ignore not set, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: nil, + ignoreReadErr: nil, + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry does not implement CorruptObjectDeleterProvider, ignore is false, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: nil, + ignoreReadErr: ptr.To(false), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: ptr.To(false)}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry does not implement CorruptObjectDeleterProvider, ignore is true, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: nil, + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{}, + unsafeFlowWant: &deletionFlowTracker{}, + unsafeAnnotationWant: true, + status: newInternalError(fmt.Errorf("no unsafe deleter provided, can not honor ignoreStoreReadErrorWithClusterBreakingPotential")), + }, + { + name: "feature enabled, registry provides a nil unsafe deleter, ignore not set, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(false), + ignoreReadErr: nil, + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry provides a nil unsafe deleter, ignore is false, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(false), + ignoreReadErr: ptr.To(false), + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: ptr.To(false)}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry provides a nil unsafe deleter, ignore is true, error expected", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(false), + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{}, + unsafeFlowWant: &deletionFlowTracker{}, + unsafeAnnotationWant: true, + status: newInternalError(fmt.Errorf("no unsafe deleter provided, can not honor ignoreStoreReadErrorWithClusterBreakingPotential")), + }, + { + name: "feature enabled, registry provides a valid unsafe deleter, ignore is not set, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: nil, + authorizer: &fakeAuthorizer{decision: authorizer.DecisionAllow, reason: "permitted"}, + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: nil}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry provides a valid unsafe deleter, ignore is false, should invoke the normal deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(false), + authorizer: &fakeAuthorizer{decision: authorizer.DecisionAllow, reason: "permitted"}, + normalFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: ptr.To(false)}, + unsafeFlowWant: &deletionFlowTracker{}, + status: success, + }, + { + name: "feature enabled, registry provides a valid unsafe deleter, ignore is true, no authorizer, error expected", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(true), + normalFlowWant: &deletionFlowTracker{}, + unsafeFlowWant: &deletionFlowTracker{}, + unsafeAnnotationWant: true, + status: newInternalError(fmt.Errorf("no authorizer provided, unable to authorize unsafe delete")), + }, + { + name: "feature enabled, registry provides a valid unsafe deleter, ignore is true, not authorized, error expected", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(true), + authorizer: &fakeAuthorizer{decision: authorizer.DecisionDeny, reason: "not permitted"}, + normalFlowWant: &deletionFlowTracker{}, + unsafeFlowWant: &deletionFlowTracker{}, + unsafeAnnotationWant: true, + status: newForbiddenError(), + }, + { + name: "feature enabled, registry provides a valid unsafe deleter, ignore is true, authorized, should invoke the unsafe deletion flow", + featureEnabled: true, + registryHasUnsafeDeleter: ptr.To(true), + ignoreReadErr: ptr.To(true), + authorizer: &fakeAuthorizer{decision: authorizer.DecisionAllow, reason: "permitted"}, + normalFlowWant: &deletionFlowTracker{}, + unsafeFlowWant: &deletionFlowTracker{Invoked: 1, Ignore: ptr.To(true)}, + unsafeAnnotationWant: true, + status: success, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AllowUnsafeMalformedObjectDeletion, test.featureEnabled) + + scope := &RequestScope{ + Namer: &mockNamer{}, + Serializer: newMetav1Serializer(t), + Authorizer: test.authorizer, + MetaGroupVersion: metav1.SchemeGroupVersion, + } + + // create a fake deleter that will be used for the normal deletion + // flow, and it will record the normal deletion activities + normalFlowObserved, unsafeFlowObserved := &deletionFlowTracker{}, &deletionFlowTracker{} + var deleter rest.GracefulDeleter = &fakeRegistry{tracker: normalFlowObserved} + if provider := test.registryHasUnsafeDeleter; provider != nil { + r := &fakeRegistryWithCorruptObjDeleter{GracefulDeleter: deleter} + if *provider { + // create a fake deleter that will be used for the unsafe deletion + // flow, and it will record the unsafe deletion activities + r.unsafeDeleter = &fakeRegistry{tracker: unsafeFlowObserved} + } + deleter = r + } + + handler := DeleteResource(deleter, true, scope, &fakeAdmitter{}) + req := newRequest(t, test.ignoreReadErr) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + // the status we have received in the response should match expectation + resp := recorder.Result() + status := toStatus(t, resp) + if want, got := test.status, status; !cmp.Equal(want, got) { + t.Errorf("expected the unsafe deletion flow observation to match, diff: %s", cmp.Diff(want, got)) + } + + // normal deletion flow + if want, got := test.normalFlowWant, normalFlowObserved; !cmp.Equal(want, got) { + t.Errorf("expected the normal deletion flow observation to match, diff: %s", cmp.Diff(want, got)) + } + // this is an invariant; if the feature is disabled the normal deletion + // flow should never see the ignore option set. + if got := normalFlowObserved; !test.featureEnabled && got.Invoked == 1 && got.Ignore != nil { + t.Errorf("IgnoreStoreReadErrorWithClusterBreakingPotential should always be nil when the feature is disabled, but got: %t", *got.Ignore) + } + + // unsafe deletion flow + if want, got := test.unsafeFlowWant, unsafeFlowObserved; !cmp.Equal(want, got) { + t.Errorf("expected the unsafe deletion flow observation to match, diff: %s", cmp.Diff(want, got)) + } + // this is an invariant; when invoked, the unsafe deletion flow should always see the option enabled + if got := ptr.Deref(unsafeFlowObserved.Ignore, false); unsafeFlowObserved.Invoked == 1 && !got { + t.Errorf("Invariant failed, IgnoreStoreReadErrorWithClusterBreakingPotential should always be set to %t for the unsafe deletion flow, but got: %t", true, got) + } + + // certain annotation should be added in unsafe delete + const keyWant = "apiserver.k8s.io/unsafe-delete-ignore-read-error" + ac := audit.AuditContextFrom(req.Context()) + switch test.unsafeAnnotationWant { + case true: + if value, ok := ac.GetEventAnnotation(keyWant); !ok || len(value) > 0 { + t.Errorf("expected annotation %q to exist with an empty value, but got exists: %t, value: %q", keyWant, ok, value) + } + default: + if value, ok := ac.GetEventAnnotation(keyWant); ok { + t.Errorf("did not expect annotation %q to exist, but got exists: %t, value: %q", keyWant, ok, value) + } + } + }) + } +} + +func TestDeleteCollectionWithUnsafeDeletionFlow(t *testing.T) { + success := metav1.Status{Status: "Success", Code: 200, Details: &metav1.StatusDetails{}} + + tests := []struct { + name string + featureEnabled bool + // what the user passes in the delete options for ignoreStoreReadErrorWithClusterBreakingPotential + ignoreReadErr *bool + // want + deleterInvoked int // how many times the deleter should be invoked + status metav1.Status // status we expect from the HTTP response + }{ + { + name: "feature disabled, ignore not set, should invoke the deleter", + featureEnabled: false, + ignoreReadErr: nil, + deleterInvoked: 1, + status: success, + }, + { + name: "feature disabled, ignore is false, should invoke the deleter", + featureEnabled: false, + ignoreReadErr: ptr.To(false), + deleterInvoked: 1, + status: success, + }, + { + name: "feature disabled, ignore is true, should invoke the deleter", + featureEnabled: false, + ignoreReadErr: ptr.To(true), + deleterInvoked: 1, + status: success, + }, + { + name: "feature enabled, ignore not set, should invoke deleter", + featureEnabled: true, + ignoreReadErr: nil, + deleterInvoked: 1, + status: success, + }, + { + name: "feature enabled, ignore is false, should invoke deleter", + featureEnabled: true, + ignoreReadErr: ptr.To(false), + deleterInvoked: 1, + status: success, + }, + { + name: "feature enabled, ignore is true, invalid error expected", + featureEnabled: true, + ignoreReadErr: ptr.To(true), + status: metav1.Status{ + TypeMeta: metav1.TypeMeta{Kind: "Status", APIVersion: "v1"}, + Status: "Failure", + Reason: "Invalid", + Code: 422, + Details: &metav1.StatusDetails{ + Group: "meta.k8s.io", + Kind: "DeleteOptions", + Causes: []metav1.StatusCause{ + { + Type: "FieldValueInvalid", + Field: "ignoreStoreReadErrorWithClusterBreakingPotential", + Message: "Invalid value: true: is not allowed with DELETECOLLECTION, try again after removing the option", + }, + }, + }, + Message: `DeleteOptions.meta.k8s.io "" is invalid: ignoreStoreReadErrorWithClusterBreakingPotential: Invalid value: true: is not allowed with DELETECOLLECTION, try again after removing the option`, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AllowUnsafeMalformedObjectDeletion, test.featureEnabled) + + scope := &RequestScope{ + Namer: &mockNamer{}, + Serializer: newMetav1Serializer(t), + ParameterCodec: runtime.NewParameterCodec(scheme), + MetaGroupVersion: metav1.SchemeGroupVersion, + } + + // the deleter should never see the ignore option enabled, + // initializing it to true, so we can verify that it is reset to nil + observed := &deletionFlowTracker{} + deleter := func(ctx context.Context, _ rest.ValidateObjectFunc, options *metav1.DeleteOptions, _ *metainternalversion.ListOptions) (runtime.Object, error) { + observed.Invoked++ + observed.Ignore = nil + if ignore := options.IgnoreStoreReadErrorWithClusterBreakingPotential; ignore != nil { + observed.Ignore = ptr.To(*ignore) + } + return nil, nil + } + + handler := DeleteCollection(fakeCollectionDeleterFunc(deleter), true, scope, &fakeAdmitter{}) + req := newRequest(t, test.ignoreReadErr) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + // the status we have received in the response should match expectation + resp := recorder.Result() + status := toStatus(t, resp) + if want, got := test.status, status; !cmp.Equal(want, got) { + t.Errorf("expected the unsafe deletion flow observation to match, diff: %s", cmp.Diff(want, got)) + } + + if want, got := test.deleterInvoked, observed.Invoked; want != got { + t.Errorf("expected the deleter to be invoked %d times, but got: %d", want, got) + } + // if invoked, the deleter should never see the ignore option enabled + if got := ptr.Deref(observed.Ignore, false); test.deleterInvoked == 1 && got { + t.Errorf("expected IgnoreStoreReadErrorWithClusterBreakingPotential to be nil for the deleter, but got: %t", got) + } + // we never expect the annotation to be added + const keyNeverWant = "apiserver.k8s.io/unsafe-delete-ignore-read-error" + ac := audit.AuditContextFrom(req.Context()) + if value, ok := ac.GetEventAnnotation(keyNeverWant); ok || len(value) > 0 { + t.Errorf("did not expect annotation %q to exist, but got exists: %t, value: %q", keyNeverWant, ok, value) + } + }) + } +} + +func newRequest(t *testing.T, ignoreReadErr *bool) *http.Request { + t.Helper() + + req, err := http.NewRequest(http.MethodDelete, "/test", io.NopCloser(strings.NewReader(""))) + if err != nil { + t.Fatalf("unexpected error creating a new http.Request: %v", err) + } + + reqInfo := &request.RequestInfo{IsResourceRequest: true} + req = req.WithContext(request.WithRequestInfo(req.Context(), reqInfo)) + + ctx := audit.WithAuditContext(req.Context()) + ac := audit.AuditContextFrom(ctx) + if err := ac.Init(audit.RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil { + t.Fatal(err) + } + req = req.WithContext(ctx) + + if ignoreReadErr != nil { + q := req.URL.Query() + q.Add("ignoreStoreReadErrorWithClusterBreakingPotential", strconv.FormatBool(*ignoreReadErr)) + req.URL.RawQuery = q.Encode() + } + + req.Header.Set("Content-Type", "application/json") + return req +} + +func newMetav1Serializer(t *testing.T) runtime.NegotiatedSerializer { + t.Helper() + + scheme := runtime.NewScheme() + metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion) + if err := metainternalversion.AddToScheme(scheme); err != nil { + t.Fatalf("metainternalversion.AddToScheme failed - err: %v", err) + } + codecs := serializer.NewCodecFactory(scheme) + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + if !ok { + t.Fatalf("failed to setup serializer") + } + return runtime.NewSimpleNegotiatedSerializer(info) +} + +func toStatus(t *testing.T, resp *http.Response) metav1.Status { + t.Helper() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error reading response body: %v", err) + } + var status metav1.Status + if err := json.Unmarshal(body, &status); err != nil { + t.Fatalf("unexpected error unmarshaling to metav1.Status: %v", err) + } + return status +} + +// to keep track of what a deletion flow (GracefulDeleter) observes +type deletionFlowTracker struct { + Invoked int // how many times Delete has been invoked + Ignore *bool // the ignore option passed to Delete +} + +// implements a fake GracefulDeleter. +// TODO: this is not thread safe if concurrent Delete calls are made by a +// test, tests are expected to invoke Delete serially, if that is to chage +// we need to implement locking to keept it concurrency safe. +type fakeRegistry struct { + tracker *deletionFlowTracker +} + +func (f *fakeRegistry) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + f.tracker.Invoked++ + f.tracker.Ignore = nil + // the following is on the basis that invoking the deleter more than once is an + // error condition, so we are just storing the final ignore option provided + if ignore := options.IgnoreStoreReadErrorWithClusterBreakingPotential; ignore != nil { + f.tracker.Ignore = ptr.To(*ignore) + } + return nil, true, nil +} + +var _ rest.CorruptObjectDeleterProvider = &fakeRegistryWithCorruptObjDeleter{} + +// fake registry implementation with support of the unsafe deletion flow +type fakeRegistryWithCorruptObjDeleter struct { + // the default GracefulDeleter in use for the normal deletion flow + rest.GracefulDeleter + // used for the unsafe deletion flow + unsafeDeleter rest.GracefulDeleter +} + +// the delete handler switches to the unsafe deletion flow by using GetCorruptObjDeleter +func (p *fakeRegistryWithCorruptObjDeleter) GetCorruptObjDeleter() rest.GracefulDeleter { + return p.unsafeDeleter +} + +type fakeAdmitter struct{} + +func (f *fakeAdmitter) Handles(_ admission.Operation) bool { return false } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/admission.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/admission.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/admission.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/admission.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/admission_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/admission_test.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/admission_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/admission_test.go index 7734aabf8..6c5df5450 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/admission_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/admission_test.go @@ -36,11 +36,11 @@ func TestAdmission(t *testing.T) { now := metav1.Now() validFieldsV1 := metav1.FieldsV1{} - var err error - validFieldsV1.Raw, err = fieldpath.NewSet(fieldpath.MakePathOrDie("metadata", "labels", "test-label")).ToJSON() + raw, err := fieldpath.NewSet(fieldpath.MakePathOrDie("metadata", "labels", "test-label")).ToJSON() if err != nil { t.Fatal(err) } + validFieldsV1.SetRawBytes(raw) validManagedFieldsEntry := metav1.ManagedFieldsEntry{ APIVersion: "v1", Operation: metav1.ManagedFieldsOperationApply, @@ -64,7 +64,7 @@ func TestAdmission(t *testing.T) { return managedFields, true }, "invalid fieldsV1": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { - managedFields.FieldsV1 = &metav1.FieldsV1{Raw: []byte("{invalid}")} + managedFields.FieldsV1 = metav1.NewFieldsV1("{invalid}") return managedFields, true }, "invalid manager": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/bench_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/bench_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/bench_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/bench_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/endpoints.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/endpoints.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/endpoints.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/endpoints.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/equality.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/equality.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/equality.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/equality.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/equality_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/equality_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/equality_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/equality_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/node.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/node.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/node.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/node.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/pod.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/pod.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/fieldmanager/pod.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/fieldmanager/pod.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/finisher/finisher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/finisher/finisher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/finisher/finisher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/finisher/finisher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/finisher/finisher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/finisher/finisher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/finisher/finisher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/finisher/finisher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/get.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/get.go similarity index 54% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/get.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/get.go index eb52baf19..134e163ee 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/get.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/get.go @@ -25,10 +25,7 @@ import ( "strings" "time" - "go.opentelemetry.io/otel/attribute" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" metainternalversionvalidation "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation" @@ -36,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" @@ -170,142 +168,163 @@ func getRequestOptions(req *http.Request, scope *RequestScope, into runtime.Obje func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { ctx := req.Context() - // For performance tracking purposes. - ctx, span := tracing.Start(ctx, "List", traceFields(req)...) - req = req.WithContext(ctx) - namespace, err := scope.Namer.Namespace(req) if err != nil { scope.err(err, w, req) return } - - // Watches for single objects are routed to this function. - // Treat a name parameter the same as a field selector entry. - hasName := true - _, name, err := scope.Namer.Name(req) - if err != nil { - hasName = false - } ctx = request.WithNamespace(ctx, namespace) - opts := metainternalversion.ListOptions{} - if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil { - err = errors.NewBadRequest(err.Error()) + outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope) + if err != nil { scope.err(err, w, req) return } - - metainternalversion.SetListOptionsDefaults(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)) - if errs := metainternalversionvalidation.ValidateListOptions(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)); len(errs) > 0 { - err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "ListOptions"}, "", errs) + opts, err := listOpts(req, scope) + if err != nil { scope.err(err, w, req) return } - outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope) + switch { + case opts.Watch || forceWatch: + err = handleWatch(ctx, rw, scope, req, w, opts, outputMediaType, minRequestTimeout) + default: + err = handleList(ctx, r, scope, req, w, opts, outputMediaType) + } if err != nil { scope.err(err, w, req) return } + } +} - // transform fields - // TODO: DecodeParametersInto should do this. - if opts.FieldSelector != nil { - fn := func(label, value string) (newLabel, newValue string, err error) { - return scope.Convertor.ConvertFieldLabel(scope.Kind, label, value) - } - if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil { - // TODO: allow bad request to set field causes based on query parameters - err = errors.NewBadRequest(err.Error()) - scope.err(err, w, req) - return - } +func listOpts(req *http.Request, scope *RequestScope) (metainternalversion.ListOptions, error) { + // Watches for single objects are routed to this function. + // Treat a name parameter the same as a field selector entry. + hasName := true + _, name, err := scope.Namer.Name(req) + if err != nil { + hasName = false + } + + opts := metainternalversion.ListOptions{} + if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, &opts); err != nil { + return opts, errors.NewBadRequest(err.Error()) + } + + metainternalversion.SetListOptionsDefaults(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)) + if errs := metainternalversionvalidation.ValidateListOptions(&opts, utilfeature.DefaultFeatureGate.Enabled(features.WatchList)); len(errs) > 0 { + err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "ListOptions"}, "", errs) + return opts, err + } + + // transform fields + // TODO: DecodeParametersInto should do this. + if opts.FieldSelector != nil { + fn := func(label, value string) (newLabel, newValue string, err error) { + return scope.Convertor.ConvertFieldLabel(scope.Kind, label, value) } + if opts.FieldSelector, err = opts.FieldSelector.Transform(fn); err != nil { + // TODO: allow bad request to set field causes based on query parameters + return opts, errors.NewBadRequest(err.Error()) + } + } - if hasName { - // metadata.name is the canonical internal name. - // SelectionPredicate will notice that this is a request for - // a single object and optimize the storage query accordingly. - nameSelector := fields.OneTermEqualSelector("metadata.name", name) - - // Note that fieldSelector setting explicitly the "metadata.name" - // will result in reaching this branch (as the value of that field - // is propagated to requestInfo as the name parameter. - // That said, the allowed field selectors in this branch are: - // nil, fields.Everything and field selector matching metadata.name - // for our name. - if opts.FieldSelector != nil && !opts.FieldSelector.Empty() { - selectedName, ok := opts.FieldSelector.RequiresExactMatch("metadata.name") - if !ok || name != selectedName { - scope.err(errors.NewBadRequest("fieldSelector metadata.name doesn't match requested name"), w, req) - return - } - } else { - opts.FieldSelector = nameSelector + if hasName { + // metadata.name is the canonical internal name. + // SelectionPredicate will notice that this is a request for + // a single object and optimize the storage query accordingly. + nameSelector := fields.OneTermEqualSelector("metadata.name", name) + + // Note that fieldSelector setting explicitly the "metadata.name" + // will result in reaching this branch (as the value of that field + // is propagated to requestInfo as the name parameter. + // That said, the allowed field selectors in this branch are: + // nil, fields.Everything and field selector matching metadata.name + // for our name. + if opts.FieldSelector != nil && !opts.FieldSelector.Empty() { + selectedName, ok := opts.FieldSelector.RequiresExactMatch("metadata.name") + if !ok || name != selectedName { + return opts, errors.NewBadRequest("fieldSelector metadata.name doesn't match requested name") } + } else { + opts.FieldSelector = nameSelector } + } + return opts, nil +} - if opts.Watch || forceWatch { - if rw == nil { - scope.err(errors.NewMethodNotSupported(scope.Resource.GroupResource(), "watch"), w, req) - return - } - // TODO: Currently we explicitly ignore ?timeout= and use only ?timeoutSeconds=. - timeout := time.Duration(0) - if opts.TimeoutSeconds != nil { - timeout = time.Duration(*opts.TimeoutSeconds) * time.Second - } - if timeout == 0 && minRequestTimeout > 0 { - timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0)) - } +func handleWatch(ctx context.Context, rw rest.Watcher, scope *RequestScope, req *http.Request, w http.ResponseWriter, opts metainternalversion.ListOptions, outputMediaType negotiation.MediaTypeOptions, minRequestTimeout time.Duration) error { + var span *tracing.Span + var onWatchListComplete WatchListCompleteHook + if isListWatchRequest(opts) { + ctx, span = tracing.Start(ctx, "WatchList", traceFields(req)...) + onWatchListComplete = func() { span.End(500 * time.Millisecond) } + req = req.WithContext(ctx) + } - klog.V(3).InfoS("Starting watch", "path", req.URL.Path, "resourceVersion", opts.ResourceVersion, "labels", opts.LabelSelector, "fields", opts.FieldSelector, "timeout", timeout) - ctx, cancel := context.WithTimeout(ctx, timeout) - defer func() { cancel() }() - watcher, err := rw.Watch(ctx, &opts) - if err != nil { - scope.err(err, w, req) - return - } - handler, err := serveWatchHandler(watcher, scope, outputMediaType, req, w, timeout, metrics.CleanListScope(ctx, &opts)) - if err != nil { - scope.err(err, w, req) - return - } - // Invalidate cancel() to defer until serve() is complete. - deferredCancel := cancel - cancel = func() {} - - serve := func() { - defer deferredCancel() - requestInfo, _ := request.RequestInfoFrom(ctx) - metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { - defer watcher.Stop() - handler.ServeHTTP(w, req) - }) - } + if rw == nil { + return errors.NewMethodNotSupported(scope.Resource.GroupResource(), "watch") + } + // TODO: Currently we explicitly ignore ?timeout= and use only ?timeoutSeconds=. + timeout := time.Duration(0) + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + if timeout == 0 && minRequestTimeout > 0 { + timeout = time.Duration(float64(minRequestTimeout) * (rand.Float64() + 1.0)) + } - // Run watch serving in a separate goroutine to allow freeing current stack memory - t := routine.TaskFrom(req.Context()) - if t != nil { - t.Func = serve - } else { - serve() - } - return - } + klog.V(3).InfoS("Starting watch", "path", req.URL.Path, "resourceVersion", opts.ResourceVersion, "labels", opts.LabelSelector, "fields", opts.FieldSelector, "sendInitialEvents", opts.SendInitialEvents, "timeout", timeout, "audit-ID", audit.GetAuditIDTruncated(ctx)) + ctx, cancel := context.WithTimeout(ctx, timeout) + defer func() { cancel() }() + watcher, err := rw.Watch(ctx, &opts) + if err != nil { + return err + } + handler, err := serveWatchHandler(watcher, scope, outputMediaType, req, w, timeout, metrics.CleanListScope(ctx, &opts), onWatchListComplete) + if err != nil { + return err + } + // Invalidate cancel() to defer until serve() is complete. + deferredCancel := cancel + cancel = func() {} - // Log only long List requests (ignore Watch). - defer span.End(500 * time.Millisecond) - span.AddEvent("About to List from storage") - result, err := r.List(ctx, &opts) - if err != nil { - scope.err(err, w, req) - return - } - span.AddEvent("Listing from storage done") - defer span.AddEvent("Writing http response done", attribute.Int("count", meta.LenList(result))) - transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result) + serve := func() { + defer deferredCancel() + requestInfo, _ := request.RequestInfoFrom(ctx) + metrics.RecordLongRunning(req, requestInfo, metrics.APIServerComponent, func() { + defer watcher.Stop() + handler.ServeHTTP(w, req) + }) } + + // Run watch serving in a separate goroutine to allow freeing current stack memory + t := routine.TaskFrom(req.Context()) + if t != nil { + t.Func = serve + } else { + serve() + } + return nil +} + +func handleList(ctx context.Context, r rest.Lister, scope *RequestScope, req *http.Request, w http.ResponseWriter, opts metainternalversion.ListOptions, outputMediaType negotiation.MediaTypeOptions) error { + // For performance tracking purposes. + ctx, span := tracing.Start(ctx, "List", traceFields(req)...) + defer span.End(500 * time.Millisecond) + req = req.WithContext(ctx) + + result, err := r.List(ctx, &opts) + if err != nil { + return err + } + transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result) + return nil +} + +// isListWatchRequest is mirrored in staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go +func isListWatchRequest(opts metainternalversion.ListOptions) bool { + return opts.SendInitialEvents != nil && *opts.SendInitialEvents && opts.AllowWatchBookmarks } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/helpers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/helpers.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/helpers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/helpers.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/helpers_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/helpers_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/helpers_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/helpers_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/namer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/namer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/namer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/namer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/negotiate.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/negotiate.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/negotiate.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/negotiate.go index 7667e6639..d97dda575 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/negotiate.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/negotiate.go @@ -168,6 +168,9 @@ type MediaTypeOptions struct { // has set Export bool + // profile controls the discovery profile (e.g., "local" for local (non peer-aggregated) discovery) + Profile string + // unrecognized is a list of all unrecognized keys Unrecognized []string @@ -227,6 +230,10 @@ func acceptMediaTypeOptions(params map[string]string, accepts *runtime.Serialize case "pretty": options.Pretty = v == "1" + // controls the discovery profile (eg local vs peer-aggregated) + case "profile": + options.Profile = v + default: options.Unrecognized = append(options.Unrecognized, k) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/negotiate_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/negotiate_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/negotiation/negotiate_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/negotiation/negotiate_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/patch.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/patch.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/patch.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/patch.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/response.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/response.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/response.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/response.go index c34c6edd5..f36bb4039 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/response.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/response.go @@ -36,7 +36,6 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" - "k8s.io/apiserver/pkg/endpoints/metrics" endpointsrequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/util/apihelpers" @@ -193,7 +192,6 @@ func (e *watchEncoder) doEncode(obj runtime.Object, event watch.Event, w io.Writ Type: string(event.Type), Object: runtime.RawExtension{Raw: e.buffer.Bytes()}, } - metrics.WatchEventsSizes.WithContext(e.ctx).WithLabelValues(e.groupVersionResource.Group, e.groupVersionResource.Version, e.groupVersionResource.Resource).Observe(float64(len(outEvent.Object.Raw))) defer e.eventBuffer.Reset() if err := e.encoder.Encode(outEvent, e.eventBuffer); err != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/response_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/response_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/response_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/response_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors.go index 07316e802..cdf77ef63 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors.go @@ -17,12 +17,12 @@ limitations under the License. package responsewriters import ( - "context" "fmt" "net/http" "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -33,10 +33,13 @@ import ( var sanitizer = strings.NewReplacer(`&`, "&", `<`, "<", `>`, ">") // Forbidden renders a simple forbidden error -func Forbidden(ctx context.Context, attributes authorizer.Attributes, w http.ResponseWriter, req *http.Request, reason string, s runtime.NegotiatedSerializer) { +func Forbidden(attributes authorizer.Attributes, w http.ResponseWriter, req *http.Request, reason string, s runtime.NegotiatedSerializer) { + RespondWithError(w, req, ForbiddenStatusError(attributes, reason), s) +} + +func RespondWithError(w http.ResponseWriter, req *http.Request, err error, s runtime.NegotiatedSerializer) { w.Header().Set("X-Content-Type-Options", "nosniff") - gv := schema.GroupVersion{Group: attributes.GetAPIGroup(), Version: attributes.GetAPIVersion()} - ErrorNegotiated(ForbiddenStatusError(attributes, reason), s, gv, w, req) + ErrorNegotiated(err, s, metav1.Unversioned, w, req) } func ForbiddenStatusError(attributes authorizer.Attributes, reason string) *apierrors.StatusError { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors_test.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors_test.go index f21582ae1..ff8730860 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/errors_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/errors_test.go @@ -78,7 +78,7 @@ func TestForbidden(t *testing.T) { observer := httptest.NewRecorder() scheme := runtime.NewScheme() negotiatedSerializer := serializer.NewCodecFactory(scheme).WithoutConversion() - Forbidden(request.NewDefaultContext(), test.attributes, observer, &http.Request{URL: &url.URL{Path: "/path"}}, test.reason, negotiatedSerializer) + Forbidden(test.attributes, observer, &http.Request{URL: &url.URL{Path: "/path"}}, test.reason, negotiatedSerializer) result := observer.Body.String() if result != test.expected { t.Errorf("Forbidden response body(%#v...)\n expected: %v\ngot: %v", test.attributes, test.expected, result) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/status.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/status.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/status.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/status.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/status_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/status_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/status_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/status_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/testdata/pod.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/testdata/pod.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/testdata/pod.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/testdata/pod.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers.go index 55f8b657a..cb782f76a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers.go @@ -34,7 +34,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" @@ -44,6 +43,7 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/apiserver/pkg/util/flushwriter" "k8s.io/component-base/tracing" + "k8s.io/streaming/pkg/httpstream/wsstream" ) // StreamObject performs input stream negotiation from a ResourceStreamer and writes that to the response. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers_test.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers_test.go index 555478a61..10bd42fcd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/responsewriters/writers_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/responsewriters/writers_test.go @@ -709,16 +709,16 @@ func toJSON(b *testing.B, list *v1.PodList) []byte { return out } -func benchmarkSerializeObject(b *testing.B, payload []byte) { - input, output := len(payload), len(gzipContent(payload, defaultGzipContentEncodingLevel)) - b.Logf("Payload size: %d, expected output size: %d, ratio: %.2f", input, output, float64(output)/float64(input)) - +func benchmarkSerializeObject(b *testing.B, payload []byte, gzip bool) { req := &http.Request{ - Header: http.Header{ - "Accept-Encoding": []string{"gzip"}, - }, URL: &url.URL{Path: "/path"}, } + if gzip { + req.Header = http.Header{ + "Accept-Encoding": []string{"gzip"}, + } + } + featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.APIResponseCompression, true) encoder := &fakeEncoder{ @@ -726,34 +726,42 @@ func benchmarkSerializeObject(b *testing.B, payload []byte) { } b.ResetTimer() - for i := 0; i < b.N; i++ { + responseBytesTotal := 0 + for b.Loop() { recorder := httptest.NewRecorder() SerializeObject("application/json", encoder, recorder, req, http.StatusOK, nil /* object */) result := recorder.Result() if result.StatusCode != http.StatusOK { b.Fatalf("incorrect status code: got %v; want: %v", result.StatusCode, http.StatusOK) } + responseBytesTotal += recorder.Body.Len() } + b.ReportMetric(float64(responseBytesTotal/b.N), "writtenBytes/op") } -func BenchmarkSerializeObject1000PodsPB(b *testing.B) { - benchmarkSerializeObject(b, toProtoBuf(b, benchmarkItems(b, "testdata/pod.json", 1000))) -} -func BenchmarkSerializeObject10000PodsPB(b *testing.B) { - benchmarkSerializeObject(b, toProtoBuf(b, benchmarkItems(b, "testdata/pod.json", 10000))) -} -func BenchmarkSerializeObject100000PodsPB(b *testing.B) { - benchmarkSerializeObject(b, toProtoBuf(b, benchmarkItems(b, "testdata/pod.json", 100000))) -} - -func BenchmarkSerializeObject1000PodsJSON(b *testing.B) { - benchmarkSerializeObject(b, toJSON(b, benchmarkItems(b, "testdata/pod.json", 1000))) -} -func BenchmarkSerializeObject10000PodsJSON(b *testing.B) { - benchmarkSerializeObject(b, toJSON(b, benchmarkItems(b, "testdata/pod.json", 10000))) -} -func BenchmarkSerializeObject100000PodsJSON(b *testing.B) { - benchmarkSerializeObject(b, toJSON(b, benchmarkItems(b, "testdata/pod.json", 100000))) +func BenchmarkSerializeObject(b *testing.B) { + for _, count := range []int{1_000, 10_000, 100_000} { + b.Run(fmt.Sprintf("Count=%d", count), func(b *testing.B) { + medias := []struct { + name string + convert func(*testing.B, *v1.PodList) []byte + }{ + {"Json", toJSON}, + {"Protobuf", toProtoBuf}, + } + podList := benchmarkItems(b, "testdata/pod.json", count) + for _, media := range medias { + b.Run(fmt.Sprintf("MediaType=%s", media.name), func(b *testing.B) { + payload := media.convert(b, podList) + for _, gzip := range []bool{true, false} { + b.Run(fmt.Sprintf("Compression=%v", gzip), func(b *testing.B) { + benchmarkSerializeObject(b, payload, gzip) + }) + } + }) + } + }) + } } type fakeResponseRecorder struct { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/rest.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/rest.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/rest.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/rest.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/rest_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/rest_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/rest_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/rest_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/trace_util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/trace_util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/trace_util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/trace_util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/update.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/update.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/update.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/update.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch.go similarity index 74% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch.go index 7bf702c61..9121ec3fa 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch.go @@ -21,27 +21,31 @@ import ( "fmt" "io" "net/http" + "sync/atomic" "time" + "go.opentelemetry.io/otel/attribute" "golang.org/x/net/websocket" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/server/httplog" "k8s.io/apiserver/pkg/storage" utilfeature "k8s.io/apiserver/pkg/util/feature" + compbasemetrics "k8s.io/component-base/metrics" + "k8s.io/component-base/tracing" + "k8s.io/klog/v2" + "k8s.io/streaming/pkg/httpstream/wsstream" ) -// nothing will ever be sent down this channel -var neverExitWatch <-chan time.Time = make(chan time.Time) - // timeoutFactory abstracts watch timeout logic for testing type TimeoutFactory interface { TimeoutCh() (<-chan time.Time, func() bool) @@ -56,7 +60,8 @@ type realTimeoutFactory struct { // and a cleanup function to call when this happens. func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) { if w.timeout == 0 { - return neverExitWatch, func() bool { return false } + // nothing will ever be sent down this channel + return nil, func() bool { return false } } t := time.NewTimer(w.timeout) return t.C, t.Stop @@ -64,7 +69,7 @@ func (w *realTimeoutFactory) TimeoutCh() (<-chan time.Time, func() bool) { // serveWatchHandler returns a handle to serve a watch response. // TODO: the functionality in this method and in WatchServer.Serve is not cleanly decoupled. -func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration, metricsScope string) (http.Handler, error) { +func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOptions negotiation.MediaTypeOptions, req *http.Request, w http.ResponseWriter, timeout time.Duration, metricsScope string, completeHook WatchListCompleteHook) (http.Handler, error) { options, err := optionsForTransform(mediaTypeOptions, req) if err != nil { return nil, err @@ -171,7 +176,8 @@ func serveWatchHandler(watcher watch.Interface, scope *RequestScope, mediaTypeOp TimeoutFactory: &realTimeoutFactory{timeout}, ServerShuttingDownCh: serverShuttingDownCh, - metricsScope: metricsScope, + metricsScope: metricsScope, + watchListCompleteHook: completeHook, } if wsstream.IsWebSocketRequest(req) { @@ -201,12 +207,47 @@ type WatchServer struct { TimeoutFactory TimeoutFactory ServerShuttingDownCh <-chan struct{} - metricsScope string + metricsScope string + watchListCompleteHook WatchListCompleteHook +} + +type WatchListCompleteHook func() + +// watchEventMetricsRecorder allows the caller to count bytes written and report the size of the event. +// It is thread-safe, as long as underlying io.Writer is thread-safe. +// Once all Write calls for a given watch event have finished, RecordEvent must be called. +type watchEventMetricsRecorder struct { + writer io.Writer + countMetric compbasemetrics.CounterMetric + sizeMetric compbasemetrics.ObserverMetric + byteCount atomic.Int64 +} + +// Write implements io.Writer. +func (c *watchEventMetricsRecorder) Write(p []byte) (n int, err error) { + n, err = c.writer.Write(p) + c.byteCount.Add(int64(n)) + return +} + +// Record reports the metrics and resets the byte count. +func (c *watchEventMetricsRecorder) RecordEvent() { + c.countMetric.Inc() + c.sizeMetric.Observe(float64(c.byteCount.Swap(0))) } // HandleHTTP serves a series of encoded events via HTTP with Transfer-Encoding: chunked. // or over a websocket connection. func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx, span := tracing.Start(ctx, "WatchServer.HandleHTTP", + attribute.String("audit-id", audit.GetAuditIDTruncated(ctx)), + attribute.String("method", req.Method), + attribute.String("url", req.URL.Path), + attribute.String("protocol", req.Proto), + attribute.String("mediaType", s.MediaType), + attribute.String("encoder", string(s.Encoder.Identifier()))) + req = req.WithContext(ctx) defer func() { if s.MemoryAllocator != nil { runtime.AllocatorPool.Put(s.MemoryAllocator) @@ -216,7 +257,7 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { flusher, ok := w.(http.Flusher) if !ok { err := fmt.Errorf("unable to start watch - can't get http.Flusher: %#v", w) - utilruntime.HandleError(err) + utilruntime.HandleErrorWithContext(req.Context(), err, "Unable to start watch") s.Scope.err(errors.NewInternalError(err), w, req) return } @@ -225,7 +266,7 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { if framer == nil { // programmer error err := fmt.Errorf("no stream framing support is available for media type %q", s.MediaType) - utilruntime.HandleError(err) + utilruntime.HandleErrorWithContext(req.Context(), err, "No stream framing support available") s.Scope.err(errors.NewBadRequest(err.Error()), w, req) return } @@ -241,10 +282,18 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { flusher.Flush() gvr := s.Scope.Resource - watchEncoder := newWatchEncoder(req.Context(), gvr, s.EmbeddedEncoder, s.Encoder, framer) + + recorder := &watchEventMetricsRecorder{ + writer: framer, + countMetric: metrics.WatchEvents.WithContext(req.Context()).WithLabelValues(gvr.Group, gvr.Version, gvr.Resource), + sizeMetric: metrics.WatchEventsSizes.WithContext(req.Context()).WithLabelValues(gvr.Group, gvr.Version, gvr.Resource), + } + + watchEncoder := newWatchEncoder(req.Context(), gvr, s.EmbeddedEncoder, s.Encoder, recorder) ch := s.Watching.ResultChan() done := req.Context().Done() + span.AddEvent("About to start writing response") for { select { case <-s.ServerShuttingDownCh: @@ -265,20 +314,33 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { // End of results. return } - metrics.WatchEvents.WithContext(req.Context()).WithLabelValues(gvr.Group, gvr.Version, gvr.Resource).Inc() - isWatchListLatencyRecordingRequired := shouldRecordWatchListLatency(event) + isWatchListLatencyRecordingRequired := shouldRecordWatchListLatency(req.Context(), event) if err := watchEncoder.Encode(event); err != nil { - utilruntime.HandleError(err) + utilruntime.HandleErrorWithContext(req.Context(), err, "Failed to encode watch event") // client disconnect. return } + recorder.RecordEvent() if len(ch) == 0 { flusher.Flush() } if isWatchListLatencyRecordingRequired { - metrics.RecordWatchListLatency(req.Context(), s.Scope.Resource, s.metricsScope) + // Record completion of initial listing phase for WatchList + receivedTimestamp, ok := apirequest.ReceivedTimestampFrom(req.Context()) + if !ok { + utilruntime.HandleErrorWithContext(req.Context(), nil, "Unable to measure watchlist latency, no received timestamp found in the context", "gvr", s.Scope.Resource) + } else { + initLatency := time.Since(receivedTimestamp) + metrics.RecordWatchListLatency(req.Context(), s.Scope.Resource, s.metricsScope, initLatency) + auditID := audit.GetAuditIDTruncated(req.Context()) + klog.V(3).InfoS("WatchList initial events sent", "path", req.URL.Path, "auditID", auditID, "initLatency", initLatency) + httplog.AddKeyValue(req.Context(), "watchlist_init_latency", initLatency) + span.AddEvent("Writing initial events done") + span.End(5 * time.Second) + s.watchListCompleteHook() + } } } } @@ -286,6 +348,9 @@ func (s *WatchServer) HandleHTTP(w http.ResponseWriter, req *http.Request) { // HandleWS serves a series of encoded events over a websocket connection. func (s *WatchServer) HandleWS(ws *websocket.Conn) { + ctx := ws.Request().Context() + logger := klog.FromContext(ctx) + defer func() { if s.MemoryAllocator != nil { runtime.AllocatorPool.Put(s.MemoryAllocator) @@ -299,10 +364,10 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) { defer cleanup() go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithLogger(logger) // This blocks until the connection is closed. // Client should not send anything. - wsstream.IgnoreReceives(ws, 0) + wsstream.IgnoreReceivesWithLogger(logger, ws, 0) // Once the client closes, we should also close close(done) }() @@ -310,7 +375,7 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) { framer := newWebsocketFramer(ws, s.UseTextFraming) gvr := s.Scope.Resource - watchEncoder := newWatchEncoder(context.TODO(), gvr, s.EmbeddedEncoder, s.Encoder, framer) + watchEncoder := newWatchEncoder(ctx, gvr, s.EmbeddedEncoder, s.Encoder, framer) ch := s.Watching.ResultChan() for { @@ -326,7 +391,7 @@ func (s *WatchServer) HandleWS(ws *websocket.Conn) { } if err := watchEncoder.Encode(event); err != nil { - utilruntime.HandleError(err) + utilruntime.HandleErrorWithLogger(logger, err, "Failed to encode watch event") // client disconnect. return } @@ -363,7 +428,7 @@ func (w *websocketFramer) Write(p []byte) (int, error) { var _ io.Writer = &websocketFramer{} -func shouldRecordWatchListLatency(event watch.Event) bool { +func shouldRecordWatchListLatency(ctx context.Context, event watch.Event) bool { if event.Type != watch.Bookmark || !utilfeature.DefaultFeatureGate.Enabled(features.WatchList) { return false } @@ -373,7 +438,7 @@ func shouldRecordWatchListLatency(event watch.Event) bool { // for more please read https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/3157-watch-list hasAnnotation, err := storage.HasInitialEventsEndBookmarkAnnotation(event.Object) if err != nil { - utilruntime.HandleError(fmt.Errorf("unable to determine if the obj has the required annotation for measuring watchlist latency, obj %T: %v", event.Object, err)) + utilruntime.HandleErrorWithContext(ctx, err, "Unable to determine if the object has the required annotation for measuring watchlist latency", "object", fmt.Sprintf("%T", event.Object)) return false } return hasAnnotation diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch_test.go similarity index 67% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch_test.go index e9277c250..1e5784e32 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/handlers/watch_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/handlers/watch_test.go @@ -17,6 +17,7 @@ limitations under the License. package handlers import ( + "bytes" "context" "encoding/json" "fmt" @@ -24,6 +25,8 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" + "sync" "testing" "time" @@ -36,9 +39,12 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/metrics" endpointstesting "k8s.io/apiserver/pkg/endpoints/testing" "k8s.io/client-go/dynamic" restclient "k8s.io/client-go/rest" + "k8s.io/component-base/metrics/legacyregistry" + metricstestutil "k8s.io/component-base/metrics/testutil" ) // Fake API versions, similar to api/latest.go @@ -341,3 +347,143 @@ func serveWatch(watcher watch.Interface, watchServer *WatchServer, preServeErr e watchServer.HandleHTTP(w, req) } } + +type fakeCachingObject struct { + obj runtime.Object + + once sync.Once + raw []byte + err error +} + +func (f *fakeCachingObject) CacheEncode(_ runtime.Identifier, encode func(runtime.Object, io.Writer) error, w io.Writer) error { + f.once.Do(func() { + buffer := bytes.NewBuffer(nil) + f.err = encode(f.obj, buffer) + f.raw = buffer.Bytes() + }) + + if f.err != nil { + return f.err + } + + _, err := w.Write(f.raw) + return err +} + +func (f *fakeCachingObject) GetObject() runtime.Object { + return f.obj +} + +func (f *fakeCachingObject) GetObjectKind() schema.ObjectKind { + return f.obj.GetObjectKind() +} + +func (f *fakeCachingObject) DeepCopyObject() runtime.Object { + return &fakeCachingObject{obj: f.obj.DeepCopyObject()} +} + +var _ runtime.CacheableObject = &fakeCachingObject{} +var _ runtime.Object = &fakeCachingObject{} + +func TestWatchEventSizes(t *testing.T) { + metrics.Register() + gvr := schema.GroupVersionResource{Group: "group", Version: "version", Resource: "resource"} + testCases := []struct { + name string + event watch.Event + wantSize int64 + }{ + { + name: "regular object", + event: watch.Event{ + Type: watch.Added, + Object: &endpointstesting.Simple{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, + }, + wantSize: 2 * 98, + }, + { + name: "cached object", + event: watch.Event{ + Type: watch.Added, + Object: &fakeCachingObject{obj: &runtime.Unknown{ + Raw: []byte(`{"kind":"Simple","apiVersion":"v1","metadata":{"name":"foo"}}`), + ContentType: runtime.ContentTypeJSON, + }}, + }, + wantSize: 2 * 88, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + + metrics.WatchEventsSizes.Reset() + metrics.WatchEvents.Reset() + + watcher := watch.NewFake() + timeoutCh := make(chan time.Time) + doneCh := make(chan struct{}) + + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + require.True(t, ok) + require.NotNil(t, info.StreamSerializer) + serializer := info.StreamSerializer + + watchServer := &WatchServer{ + Scope: &RequestScope{ + Resource: gvr, + }, + Watching: watcher, + MediaType: "application/json", + Framer: serializer.Framer, + Encoder: testCodecV2, + EmbeddedEncoder: testCodecV2, + TimeoutFactory: &fakeTimeoutFactory{timeoutCh: timeoutCh, done: doneCh}, + } + + s := httptest.NewServer(serveWatch(watcher, watchServer, nil)) + defer s.Close() + + // Setup a client + dest, _ := url.Parse(s.URL) + dest.Path = "/" + namedGroupPrefix + "/" + testGroupV2.Group + "/" + testGroupV2.Version + "/simple" + dest.RawQuery = "watch=true" + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, dest.String(), nil) + client := http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer apitesting.Close(t, resp.Body) + + // Send object twice so that in case of caching the cached version is used. + watcher.Action(tc.event.Type, tc.event.Object) + watcher.Action(tc.event.Type, tc.event.Object) + + close(timeoutCh) + <-doneCh + + expected := fmt.Sprintf(`# HELP apiserver_watch_events_sizes [ALPHA] Watch event size distribution in bytes +# TYPE apiserver_watch_events_sizes histogram +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="1024"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="2048"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="4096"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="8192"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="16384"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="32768"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="65536"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="131072"} 2 +apiserver_watch_events_sizes_bucket{group="group",resource="resource",version="version",le="+Inf"} 2 +apiserver_watch_events_sizes_sum{group="group",resource="resource",version="version"} %d +apiserver_watch_events_sizes_count{group="group",resource="resource",version="version"} 2 + +# HELP apiserver_watch_events_total [ALPHA] Number of events sent in watch clients +# TYPE apiserver_watch_events_total counter +apiserver_watch_events_total{group="group",resource="resource",version="version"} 2 +`, tc.wantSize) + + err = metricstestutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_watch_events_sizes", "apiserver_watch_events_total") + require.NoError(t, err) + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/installer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/installer.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/installer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/installer.go index 2a5900a83..56c392fd0 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/installer.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/installer.go @@ -28,6 +28,8 @@ import ( restful "github.com/emicklei/go-restful/v3" "sigs.k8s.io/structured-merge-diff/v6/fieldpath" + "golang.org/x/text/cases" + "golang.org/x/text/language" apidiscoveryv2 "k8s.io/api/apidiscovery/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" @@ -818,7 +820,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.GET(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("read"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("read"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). Writes(producedObject) @@ -839,7 +841,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.GET(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("list"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("list"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), allMediaTypes...)...). Returns(http.StatusOK, "OK", versionedList). Writes(versionedList) @@ -872,7 +874,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.PUT(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("replace"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("replace"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). // TODO: in some cases, the API may return a v1.Status instead of the versioned object @@ -905,7 +907,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). Consumes(supportedTypes...). - Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("patch"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). // Patch can return 201 when a server side apply is requested @@ -934,7 +936,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.POST(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("create"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("create"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). // TODO: in some cases, the API may return a v1.Status instead of the versioned object @@ -963,7 +965,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.DELETE(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("delete"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("delete"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Writes(deleteReturnType). Returns(http.StatusOK, "OK", deleteReturnType). @@ -987,7 +989,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.DELETE(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("deletecollection"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("deletecollection"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Writes(versionedStatus). Returns(http.StatusOK, "OK", versionedStatus) @@ -1015,7 +1017,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.GET(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("watch"+namespaced+kind+strings.Title(subresource)+operationSuffix). + Operation("watch"+namespaced+kind+cases.Title(language.English).String(subresource)+operationSuffix). Produces(allMediaTypes...). Returns(http.StatusOK, "OK", versionedWatchEvent). Writes(versionedWatchEvent) @@ -1036,7 +1038,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.GET(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).")). - Operation("watch"+namespaced+kind+strings.Title(subresource)+"List"+operationSuffix). + Operation("watch"+namespaced+kind+cases.Title(language.English).String(subresource)+"List"+operationSuffix). Produces(allMediaTypes...). Returns(http.StatusOK, "OK", versionedWatchEvent). Writes(versionedWatchEvent) @@ -1060,7 +1062,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag route := ws.Method(method).Path(action.Path). To(handler). Doc(doc). - Operation("connect" + strings.Title(strings.ToLower(method)) + namespaced + kind + strings.Title(subresource) + operationSuffix). + Operation("connect" + cases.Title(language.English).String(strings.ToLower(method)) + namespaced + kind + cases.Title(language.English).String(subresource) + operationSuffix). Produces("*/*"). Consumes("*/*"). Writes(connectProducedObject) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/installer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/installer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/installer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/installer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/metrics/metrics.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/metrics/metrics.go index b95e551d8..a4da6ebe2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/metrics/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/metrics/metrics.go @@ -18,7 +18,6 @@ package metrics import ( "context" - "fmt" "net/http" "net/url" "strconv" @@ -32,7 +31,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilsets "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authentication/user" @@ -291,7 +289,7 @@ var ( Name: "watch_list_duration_seconds", Help: "Response latency distribution in seconds for watch list requests broken by group, version, resource and scope.", Buckets: []float64{0.05, 0.1, 0.2, 0.4, 0.6, 0.8, 1.0, 2, 4, 6, 8, 10, 15, 20, 30, 45, 60}, - StabilityLevel: compbasemetrics.ALPHA, + StabilityLevel: compbasemetrics.BETA, }, []string{"group", "version", "resource", "scope"}, ) @@ -574,15 +572,8 @@ func RecordLongRunning(req *http.Request, requestInfo *request.RequestInfo, comp } // RecordWatchListLatency simply records response latency for watch list requests. -func RecordWatchListLatency(ctx context.Context, gvr schema.GroupVersionResource, metricsScope string) { - requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(ctx) - if !ok { - utilruntime.HandleError(fmt.Errorf("unable to measure watchlist latency because no received ts found in the ctx, gvr: %s", gvr)) - return - } - elapsedSeconds := time.Since(requestReceivedTimestamp).Seconds() - - watchListLatencies.WithContext(ctx).WithLabelValues(gvr.Group, gvr.Version, gvr.Resource, metricsScope).Observe(elapsedSeconds) +func RecordWatchListLatency(ctx context.Context, gvr schema.GroupVersionResource, metricsScope string, elapsed time.Duration) { + watchListLatencies.WithContext(ctx).WithLabelValues(gvr.Group, gvr.Version, gvr.Resource, metricsScope).Observe(elapsed.Seconds()) } // MonitorRequest handles standard transformations for client and the reported verb and then invokes Monitor to record @@ -617,7 +608,7 @@ func MonitorRequest(req *http.Request, verb, group, version, resource, subresour fieldValidation := cleanFieldValidation(req.URL) fieldValidationRequestLatencies.WithContext(req.Context()).WithLabelValues(fieldValidation) - if wd, ok := request.LatencyTrackersFrom(req.Context()); ok { + if wd, ok := request.LatencyTrackersFrom(req.Context()); ok && dryRun == "" { sliLatency := elapsedSeconds - (wd.MutatingWebhookTracker.GetLatency() + wd.ValidatingWebhookTracker.GetLatency() + wd.APFQueueWaitTracker.GetLatency()).Seconds() requestSloLatencies.WithContext(req.Context()).WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(sliLatency) requestSliLatencies.WithContext(req.Context()).WithLabelValues(reportedVerb, group, version, resource, subresource, scope, component).Observe(sliLatency) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi.go similarity index 89% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi.go index 0da1ab05c..75fbc6abc 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi.go @@ -19,7 +19,6 @@ package openapi import ( "bytes" "fmt" - "reflect" "sort" "strings" "unicode" @@ -29,6 +28,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" "k8s.io/kube-openapi/pkg/util" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -133,31 +133,28 @@ func gvkConvert(gvk schema.GroupVersionKind) v1.GroupVersionKind { } } -func typeName(t reflect.Type) string { - path := t.PkgPath() - if strings.Contains(path, "/vendor/") { - path = path[strings.Index(path, "/vendor/")+len("/vendor/"):] - } - return fmt.Sprintf("%s.%s", path, t.Name()) -} - // NewDefinitionNamer constructs a new DefinitionNamer to be used to customize OpenAPI spec. func NewDefinitionNamer(schemes ...*runtime.Scheme) *DefinitionNamer { ret := &DefinitionNamer{ typeGroupVersionKinds: map[string]groupVersionKinds{}, } for _, s := range schemes { - for gvk, rtype := range s.AllKnownTypes() { + for gvk := range s.AllKnownTypes() { newGVK := gvkConvert(gvk) exists := false - for _, existingGVK := range ret.typeGroupVersionKinds[typeName(rtype)] { + name, err := s.ToOpenAPIDefinitionName(gvk) + if err != nil { + klog.Fatalf("failed to get OpenAPI definition name for %v: %v", gvk, err) + continue + } + for _, existingGVK := range ret.typeGroupVersionKinds[name] { if newGVK == existingGVK { exists = true break } } if !exists { - ret.typeGroupVersionKinds[typeName(rtype)] = append(ret.typeGroupVersionKinds[typeName(rtype)], newGVK) + ret.typeGroupVersionKinds[name] = append(ret.typeGroupVersionKinds[name], newGVK) } } } @@ -170,9 +167,9 @@ func NewDefinitionNamer(schemes ...*runtime.Scheme) *DefinitionNamer { // GetDefinitionName returns the name and tags for a given definition func (d *DefinitionNamer) GetDefinitionName(name string) (string, spec.Extensions) { if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok { - return util.ToRESTFriendlyName(name), spec.Extensions{ + return name, spec.Extensions{ extensionGVK: groupVersionKinds.JSON(), } } - return util.ToRESTFriendlyName(name), nil + return name, nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi_test.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi_test.go index 6e6c4387c..ca9a6a4c2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/openapi_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/openapi_test.go @@ -42,12 +42,11 @@ func TestGetDefinitionName(t *testing.T) { // in production, the name is stripped of ".*vendor/" prefix before passed // to GetDefinitionName, so here typePkgName does not have the // "k8s.io/kubernetes/vendor" prefix. - typePkgName := "k8s.io/apiserver/pkg/endpoints/openapi/testing.TestType" typeFriendlyName := "io.k8s.apiserver.pkg.endpoints.openapi.testing.TestType" s := runtime.NewScheme() s.AddKnownTypeWithName(testType.GroupVersionKind(), &testType) namer := NewDefinitionNamer(s) - n, e := namer.GetDefinitionName(typePkgName) + n, e := namer.GetDefinitionName(typeFriendlyName) assertEqual(t, typeFriendlyName, n) assertEqual(t, []interface{}{ map[string]interface{}{ @@ -56,7 +55,7 @@ func TestGetDefinitionName(t *testing.T) { "kind": "TestType", }, }, e["x-kubernetes-group-version-kind"]) - n, e2 := namer.GetDefinitionName("test.com/another.Type") + n, e2 := namer.GetDefinitionName("com.test.another.Type") assertEqual(t, "com.test.another.Type", n) assertEqual(t, e2, spec.Extensions(nil)) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/testing/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/testing/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/testing/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/testing/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/testing/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/testing/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/openapi/testing/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/openapi/testing/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/patchhandler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/patchhandler_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/patchhandler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/patchhandler_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/context.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/context.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/context.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/context.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/context_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/context_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/context_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/context_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/methods.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/methods.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/methods.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/methods.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/received_time.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/received_time.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/received_time.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/received_time.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/received_time_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/received_time_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/received_time_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/received_time_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/requestinfo.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/requestinfo.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/requestinfo.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/requestinfo.go index d6bf062a1..1cf14d892 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/requestinfo.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/requestinfo.go @@ -22,7 +22,7 @@ import ( "net/http" "strings" - "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/api/validate/content" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -246,7 +246,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er if opts.FieldSelector != nil { if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok { - if len(path.IsValidPathSegmentName(name)) == 0 { + if len(content.IsPathSegmentName(name)) == 0 { requestInfo.Name = name } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/requestinfo_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/requestinfo_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/requestinfo_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/requestinfo_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/server_shutdown_signal.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/server_shutdown_signal.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/server_shutdown_signal.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/server_shutdown_signal.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/webhook_duration.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/webhook_duration.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/webhook_duration.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/webhook_duration.go index bda43b617..df0ff77d8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/webhook_duration.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/webhook_duration.go @@ -165,6 +165,10 @@ type LatencyTrackers struct { // When called multiple times, the latency incurred inside to // decode func each time will be summed up. DecodeTracker DurationTracker + + // ImpersonationTracker tracks the latency incurred in resolving impersonation. + // This includes mode selection, authorization checks, and cache lookups. + ImpersonationTracker DurationTracker } type latencyTrackersKeyType int @@ -193,6 +197,7 @@ func WithLatencyTrackersAndCustomClock(parent context.Context, c clock.Clock) co SerializationTracker: newSumLatencyTracker(c), ResponseWriteTracker: newSumLatencyTracker(c), DecodeTracker: newSumLatencyTracker(c), + ImpersonationTracker: newSumLatencyTracker(c), }) } @@ -286,6 +291,14 @@ func TrackDecodeLatency(ctx context.Context, d time.Duration) { } } +// TrackImpersonationLatency is used to track latency incurred +// in resolving impersonation for the request. +func TrackImpersonationLatency(ctx context.Context, d time.Duration) { + if tracker, ok := LatencyTrackersFrom(ctx); ok { + tracker.ImpersonationTracker.TrackDuration(d) + } +} + // AuditAnnotationsFromLatencyTrackers will inspect each latency tracker // associated with the request context and return a set of audit // annotations that can be added to the API audit entry. @@ -301,6 +314,7 @@ func AuditAnnotationsFromLatencyTrackers(ctx context.Context) map[string]string apfQueueWaitLatencyKey = "apiserver.latency.k8s.io/apf-queue-wait" authenticationLatencyKey = "apiserver.latency.k8s.io/authentication" authorizationLatencyKey = "apiserver.latency.k8s.io/authorization" + impersonationLatencyKey = "apiserver.latency.k8s.io/impersonation" ) tracker, ok := LatencyTrackersFrom(ctx) @@ -339,5 +353,8 @@ func AuditAnnotationsFromLatencyTrackers(ctx context.Context) map[string]string if latency := tracker.AuthorizationTracker.GetLatency(); latency != 0 { annotations[authorizationLatencyKey] = latency.String() } + if latency := tracker.ImpersonationTracker.GetLatency(); latency != 0 { + annotations[impersonationLatencyKey] = latency.String() + } return annotations } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/webhook_duration_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/webhook_duration_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/request/webhook_duration_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/request/webhook_duration_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/fake.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/fake.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/fake.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/fake.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/wrapper.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/wrapper.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/wrapper.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/wrapper.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/wrapper_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/wrapper_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/responsewriter/wrapper_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/responsewriter/wrapper_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/conversion.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/conversion.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/conversion.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/conversion.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/testing/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/testing/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/warning/warning.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/warning/warning.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/warning/warning.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/warning/warning.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/watch_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/watch_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/endpoints/watch_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/endpoints/watch_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/features/kube_features.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/features/kube_features.go similarity index 76% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/features/kube_features.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/features/kube_features.go index e04f1d468..dbe776e8a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/features/kube_features.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/features/kube_features.go @@ -109,12 +109,59 @@ const ( // Allow the API server to serve consistent lists from cache ConsistentListFromCache featuregate.Feature = "ConsistentListFromCache" + // owner: @enj @qiujian16 + // kep: https://kep.k8s.io/5284 + // + // Enables impersonation that is constrained to specific requests instead of being all or nothing. + ConstrainedImpersonation featuregate.Feature = "ConstrainedImpersonation" + // owner: @jefftree // kep: https://kep.k8s.io/4355 // // Enables coordinated leader election in the API server CoordinatedLeaderElection featuregate.Feature = "CoordinatedLeaderElection" + // owner: @jpbetz @aaron-prindle @yongruilin + // kep: http://kep.k8s.io/5073 + // beta: v1.33 + // + // Enables running declarative validation of APIs, where declared. When enabled, APIs with + // declarative validation rules will validate objects using the generated + // declarative validation code and compare the results to the regular imperative validation. + // See DeclarativeValidationBeta for more. + DeclarativeValidation featuregate.Feature = "DeclarativeValidation" + + // owner: @jpbetz @aaron-prindle @yongruilin + // kep: http://kep.k8s.io/5073 + // beta: v1.36 + // + // This feature gate acts as the Global Safety Switch for Beta-stage validation rules (+k8s:beta). + // It allows cluster admins to disable enforcement for validations in the Beta stage if + // regressions are found, forcing them back to Shadow mode. + // In Shadow mode, declarative validation is executed and mismatches against handwritten + // validation are logged as metrics, but failures do not reject requests. + // Handwritten validation remains authoritative and enforced. + // Enforcement logic for resources using WithDeclarativeEnforcement(): + // - Standard tags (no prefix): Always Enforced (Bypasses this gate). + // - Beta tags (+k8s:beta): Enforced when this gate is enabled (default), otherwise Shadowed. + // - Alpha tags (+k8s:alpha): Always Shadowed. + // This gate has no effect if the master DeclarativeValidation feature gate is disabled. + DeclarativeValidationBeta featuregate.Feature = "DeclarativeValidationBeta" + + // owner: @jpbetz @aaron-prindle @yongruilin + // kep: http://kep.k8s.io/5073 + // beta: v1.33 + // + // Deprecated: in favor of DeclarativeValidationBeta. + // + // When enabled, declarative validation errors are returned directly to the caller, + // replacing hand-written validation errors for rules that have declarative implementations. + // When disabled, hand-written validation errors are always returned, effectively putting + // declarative validation in a "shadow mode" that monitors but does not affect API responses. + // Note: Although declarative validation aims for functional equivalence with hand-written validation, + // the exact number, format, and content of error messages may differ between the two approaches. + DeclarativeValidationTakeover featuregate.Feature = "DeclarativeValidationTakeover" + // owner: @serathius // kep: https://kep.k8s.io/4988 // @@ -134,6 +181,15 @@ const ( // Enables generating snapshots of watch cache store and using them to serve LIST requests. ListFromCacheSnapshot featuregate.Feature = "ListFromCacheSnapshot" + // owner: @aramase @BenTheElder + // kep: https://kep.k8s.io/5793 + // + // Enables manifest-based admission control configuration for webhooks and policies. + // When enabled, admission webhooks and policies can be loaded from + // manifest files on disk at API server startup, providing bootstrap-time enforcement + // and protection against API-based modification. + ManifestBasedAdmissionControlConfig featuregate.Feature = "ManifestBasedAdmissionControlConfig" + // owner: @alexzielenski, @cici37, @jiahuif, @jpbetz // kep: https://kep.k8s.io/3962 // @@ -171,6 +227,14 @@ const ( // This prevents watch cache from being starved by other watches. SeparateCacheWatchRPC featuregate.Feature = "SeparateCacheWatchRPC" + // owner: @jefftree + // kep: https://kep.k8s.io/5866 + // + // Enables the shard selector parameter on List/Watch requests, + // allowing clients to receive a filtered subset of objects based + // on hash ranges of metadata fields (e.g. UID). + ShardedListAndWatch featuregate.Feature = "ShardedListAndWatch" + // owner: @serathius // // Enables APF to use size of objects for estimating request cost. @@ -195,22 +259,6 @@ const ( // Allow API server Protobuf encoder to encode collections item by item, instead of all at once. StreamingCollectionEncodingToProtobuf featuregate.Feature = "StreamingCollectionEncodingToProtobuf" - // owner: @cici37 - // - // StrictCostEnforcementForVAP is used to apply strict CEL cost validation for ValidatingAdmissionPolicy. - // It will be set to off by default for certain time of period to prevent the impact on the existing users. - // It is strongly recommended to enable this feature gate as early as possible. - // The strict cost is specific for the extended libraries whose cost defined under k8s/apiserver/pkg/cel/library. - StrictCostEnforcementForVAP featuregate.Feature = "StrictCostEnforcementForVAP" - - // owner: @cici37 - // - // StrictCostEnforcementForWebhooks is used to apply strict CEL cost validation for matchConditions in Webhooks. - // It will be set to off by default for certain time of period to prevent the impact on the existing users. - // It is strongly recommended to enable this feature gate as early as possible. - // The strict cost is specific for the extended libraries whose cost defined under k8s/apiserver/pkg/cel/library. - StrictCostEnforcementForWebhooks featuregate.Feature = "StrictCostEnforcementForWebhooks" - // owner: @aramase, @enj, @nabokihms // kep: https://kep.k8s.io/3331 // @@ -223,11 +271,11 @@ const ( // Enables Egress Selector in Structured Authentication Configuration StructuredAuthenticationConfigurationEgressSelector featuregate.Feature = "StructuredAuthenticationConfigurationEgressSelector" - // owner: @palnabarun - // kep: https://kep.k8s.io/3221 + // owner: @aramase, @enj, @nabokihms + // kep: https://kep.k8s.io/3331 // - // Enables Structured Authorization Configuration - StructuredAuthorizationConfiguration featuregate.Feature = "StructuredAuthorizationConfiguration" + // Enables JWKs metrics for Structured Authentication Configuration + StructuredAuthenticationConfigurationJWKSMetrics featuregate.Feature = "StructuredAuthenticationConfigurationJWKSMetrics" // owner: @aramase // @@ -256,16 +304,16 @@ const ( // clients. UnauthenticatedHTTP2DOSMitigation featuregate.Feature = "UnauthenticatedHTTP2DOSMitigation" + // owner: @richabanker + // + // Proxies client to an apiserver capable of serving the request in the event of version skew. + UnknownVersionInteroperabilityProxy featuregate.Feature = "UnknownVersionInteroperabilityProxy" + // owner: @wojtek-t // // Enables post-start-hook for storage readiness WatchCacheInitializationPostStartHook featuregate.Feature = "WatchCacheInitializationPostStartHook" - // owner: @serathius - // Enables watches without resourceVersion to be served from storage. - // Used to prevent https://github.com/kubernetes/kubernetes/issues/123072 until etcd fixes the issue. - WatchFromStorageWithoutResourceVersion featuregate.Feature = "WatchFromStorageWithoutResourceVersion" - // owner: @p0lyn0mial // // Allow the API server to stream individual items instead of chunking @@ -307,6 +355,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate AggregatedDiscoveryRemoveBetaType: { {Version: version.MustParse("1.0"), Default: false, PreRelease: featuregate.GA}, {Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Deprecated}, + {Version: version.MustParse("1.35"), Default: true, PreRelease: featuregate.Deprecated, LockToDefault: true}, }, AllowParsingUserUIDFromCertAuth: { @@ -348,11 +397,30 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, }, + ConstrainedImpersonation: { + {Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha}, + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta}, + }, + CoordinatedLeaderElection: { {Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Beta}, }, + DeclarativeValidation: { + {Version: version.MustParse("1.33"), Default: true, PreRelease: featuregate.Beta}, + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA and LockToDefault in 1.36, remove in 1.39 + }, + + DeclarativeValidationBeta: { + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta}, + }, + + DeclarativeValidationTakeover: { + {Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Beta}, + {Version: version.MustParse("1.36"), Default: false, PreRelease: featuregate.Deprecated}, + }, + DetectCacheInconsistency: { {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.Beta}, }, @@ -368,9 +436,14 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.Beta}, }, + ManifestBasedAdmissionControlConfig: { + {Version: version.MustParse("1.36"), Default: false, PreRelease: featuregate.Alpha}, + }, + MutatingAdmissionPolicy: { {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.34"), Default: false, PreRelease: featuregate.Beta}, + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.GA}, }, OpenAPIEnums: { @@ -397,6 +470,11 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate SeparateCacheWatchRPC: { {Version: version.MustParse("1.28"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Deprecated}, + {Version: version.MustParse("1.36"), Default: false, LockToDefault: true, PreRelease: featuregate.Deprecated}, + }, + + ShardedListAndWatch: { + {Version: version.MustParse("1.36"), Default: false, PreRelease: featuregate.Alpha}, }, SizeBasedListCostEstimate: { @@ -422,16 +500,6 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, }, - StrictCostEnforcementForVAP: { - {Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta}, - {Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, - }, - - StrictCostEnforcementForWebhooks: { - {Version: version.MustParse("1.30"), Default: false, PreRelease: featuregate.Beta}, - {Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, - }, - StructuredAuthenticationConfiguration: { {Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, @@ -442,10 +510,8 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.Beta}, }, - StructuredAuthorizationConfiguration: { - {Version: version.MustParse("1.29"), Default: false, PreRelease: featuregate.Alpha}, - {Version: version.MustParse("1.30"), Default: true, PreRelease: featuregate.Beta}, - {Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, + StructuredAuthenticationConfigurationJWKSMetrics: { + {Version: version.MustParse("1.35"), Default: true, PreRelease: featuregate.Beta}, }, TokenRequestServiceAccountUIDValidation: { @@ -457,13 +523,14 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate {Version: version.MustParse("1.29"), Default: true, PreRelease: featuregate.Beta}, }, - WatchCacheInitializationPostStartHook: { - {Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta}, + UnknownVersionInteroperabilityProxy: { + {Version: version.MustParse("1.28"), Default: false, PreRelease: featuregate.Alpha}, + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta}, }, - WatchFromStorageWithoutResourceVersion: { - {Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Beta}, - {Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Deprecated, LockToDefault: true}, + WatchCacheInitializationPostStartHook: { + {Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta}, + {Version: version.MustParse("1.36"), Default: true, PreRelease: featuregate.Beta}, }, WatchList: { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/configuration.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/configuration.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/configuration.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/configuration.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/evaluator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/evaluator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/evaluator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/evaluator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/evaluator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/evaluator_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/evaluator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/evaluator_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/registry.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/registry.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/generic/registry.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/generic/registry.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/resources.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/resources.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/resources.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/resources.go index b66471920..6fa01fc61 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/resources.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/resources.go @@ -17,7 +17,7 @@ limitations under the License. package v1 import ( - "sort" + "slices" "strings" corev1 "k8s.io/api/core/v1" @@ -198,7 +198,7 @@ func Intersection(a []corev1.ResourceName, b []corev1.ResourceName) []corev1.Res } result = append(result, item) } - sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + slices.Sort(result) return result } @@ -211,7 +211,7 @@ func Difference(a []corev1.ResourceName, b []corev1.ResourceName) []corev1.Resou } result = append(result, item) } - sort.Slice(result, func(i, j int) bool { return result[i] < result[j] }) + slices.Sort(result) return result } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/resources_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/resources_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/quota/v1/resources_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/quota/v1/resources_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease.go index 5d2e73b54..450a6e5ec 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease.go @@ -78,9 +78,9 @@ type peerEndpointLeaseReconciler struct { // NewPeerEndpointLeaseReconciler creates a new peer endpoint lease reconciler func NewPeerEndpointLeaseReconciler(config *storagebackend.ConfigForResource, baseKey string, leaseTime time.Duration) (PeerEndpointLeaseReconciler, error) { - // note that newFunc, newListFunc and resourcePrefix + // note that newFunc, newListFunc // can be left blank unless the storage.Watch method is used - leaseStorage, destroyFn, err := storagefactory.Create(*config, nil, nil, "") + leaseStorage, destroyFn, err := storagefactory.Create(*config, nil, nil, baseKey) if err != nil { return nil, fmt.Errorf("error creating storage factory: %v", err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease_test.go similarity index 82% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease_test.go index 242cadb60..c4ac0bad3 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/reconcilers/peer_endpoint_lease_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/reconcilers/peer_endpoint_lease_test.go @@ -59,14 +59,11 @@ type serverInfo struct { expectEndpoint string } -func NewFakePeerEndpointReconciler(t *testing.T, s storage.Interface) peerEndpointLeaseReconciler { - // use the same base key used by the controlplane, but add a random - // prefix so we can reuse the etcd instance for subtests independently. - base := "/" + uuid.New().String() + "/peerserverleases/" +func NewFakePeerEndpointReconciler(t *testing.T, baseKey string, s storage.Interface) peerEndpointLeaseReconciler { return peerEndpointLeaseReconciler{serverLeases: &peerEndpointLeases{ storage: s, destroyFn: func() {}, - baseKey: base, + baseKey: baseKey, leaseTime: 1 * time.Minute, // avoid the lease to timeout on tests }} } @@ -82,8 +79,10 @@ func (f *peerEndpointLeaseReconciler) SetKeys(servers []serverInfo) error { func TestPeerEndpointLeaseReconciler(t *testing.T) { // enable feature flags - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageVersionAPI, true) + featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{ + features.APIServerIdentity: true, + features.StorageVersionAPI: true, + }) server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t) t.Cleanup(func() { server.Terminate(t) }) @@ -92,12 +91,6 @@ func TestPeerEndpointLeaseReconciler(t *testing.T) { newListFunc := func() runtime.Object { return &corev1.EndpointsList{} } sc.Codec = apitesting.TestStorageCodec(codecs, corev1.SchemeGroupVersion) - s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "endpoints"}), newFunc, newListFunc, "") - if err != nil { - t.Fatalf("Error creating storage: %v", err) - } - t.Cleanup(dFunc) - tests := []struct { testName string servers []serverInfo @@ -148,8 +141,17 @@ func TestPeerEndpointLeaseReconciler(t *testing.T) { } for _, test := range tests { t.Run(test.testName, func(t *testing.T) { - fakeReconciler := NewFakePeerEndpointReconciler(t, s) - err := fakeReconciler.SetKeys(test.servers) + // use the same base key used by the controlplane, but add a random + // prefix so we can reuse the etcd instance for subtests independently. + baseKey := "/" + uuid.New().String() + "/peerserverleases/" + s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "endpoints"}), newFunc, newListFunc, baseKey) + if err != nil { + t.Fatalf("Error creating storage: %v", err) + } + t.Cleanup(dFunc) + + fakeReconciler := NewFakePeerEndpointReconciler(t, baseKey, s) + err = fakeReconciler.SetKeys(test.servers) if err != nil { t.Errorf("unexpected error creating keys: %v", err) } @@ -189,8 +191,10 @@ func TestPeerEndpointLeaseReconciler(t *testing.T) { func TestPeerLeaseRemoveEndpoints(t *testing.T) { // enable feature flags - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.APIServerIdentity, true) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageVersionAPI, true) + featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{ + features.APIServerIdentity: true, + features.StorageVersionAPI: true, + }) server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t) t.Cleanup(func() { server.Terminate(t) }) @@ -199,12 +203,6 @@ func TestPeerLeaseRemoveEndpoints(t *testing.T) { newListFunc := func() runtime.Object { return &corev1.EndpointsList{} } sc.Codec = apitesting.TestStorageCodec(codecs, corev1.SchemeGroupVersion) - s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "pods"}), newFunc, newListFunc, "") - if err != nil { - t.Fatalf("Error creating storage: %v", err) - } - t.Cleanup(dFunc) - stopTests := []struct { testName string servers []serverInfo @@ -247,8 +245,17 @@ func TestPeerLeaseRemoveEndpoints(t *testing.T) { } for _, test := range stopTests { t.Run(test.testName, func(t *testing.T) { - fakeReconciler := NewFakePeerEndpointReconciler(t, s) - err := fakeReconciler.SetKeys(test.servers) + // use the same base key used by the controlplane, but add a random + // prefix so we can reuse the etcd instance for subtests independently. + baseKey := "/" + uuid.New().String() + "/peerserverleases/" + s, dFunc, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "pods"}), newFunc, newListFunc, baseKey) + if err != nil { + t.Fatalf("Error creating storage: %v", err) + } + t.Cleanup(dFunc) + + fakeReconciler := NewFakePeerEndpointReconciler(t, baseKey, s) + err = fakeReconciler.SetKeys(test.servers) if err != nil { t.Errorf("unexpected error creating keys: %v", err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/matcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/matcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/matcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/matcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/options.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/options.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/options.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/options.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/corrupt_obj_deleter.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/corrupt_obj_deleter.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/corrupt_obj_deleter.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/corrupt_obj_deleter.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/corrupt_obj_deleter_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/corrupt_obj_deleter_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/corrupt_obj_deleter_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/corrupt_obj_deleter_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/decorated_watcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/decorated_watcher.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/decorated_watcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/decorated_watcher.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/decorated_watcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/decorated_watcher_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/decorated_watcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/decorated_watcher_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/dryrun.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/dryrun.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/dryrun.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/dryrun.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/dryrun_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/dryrun_test.go similarity index 78% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/dryrun_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/dryrun_test.go index ba0dcaaf1..7692add2f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/dryrun_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/dryrun_test.go @@ -39,7 +39,7 @@ import ( func NewDryRunnableTestStorage(t *testing.T) (DryRunnableStorage, func()) { server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t) sc.Codec = apitesting.TestStorageCodec(codecs, examplev1.SchemeGroupVersion) - s, destroy, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "pods"}), nil, nil, "") + s, destroy, err := factory.Create(*sc.ForResource(schema.GroupResource{Resource: "pods"}), nil, nil, "/pods") if err != nil { t.Fatalf("Error creating storage: %v", err) } @@ -65,12 +65,12 @@ func TestDryRunCreateDoesntCreate(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, true) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, true) if err != nil { t.Fatalf("Failed to create new dry-run object: %v", err) } - err = s.Get(context.Background(), "key", storage.GetOptions{}, out) + err = s.Get(context.Background(), "/pods/key", storage.GetOptions{}, out) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound { t.Errorf("Expected key to be not found, error: %v", err) } @@ -83,7 +83,7 @@ func TestDryRunCreateReturnsObject(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, true) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, true) if err != nil { t.Fatalf("Failed to create new dry-run object: %v", err) } @@ -100,12 +100,12 @@ func TestDryRunCreateExistingObjectFails(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } - err = s.Create(context.Background(), "key", obj, out, 0, true) + err = s.Create(context.Background(), "/pods/key", obj, out, 0, true) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyExists { t.Errorf("Expected KeyExists error: %v", err) } @@ -122,7 +122,7 @@ func TestDryRunUpdateMissingObjectFails(t *testing.T) { return input, nil, errors.New("UpdateFunction shouldn't be called") } - err := s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true, nil) + err := s.GuaranteedUpdate(context.Background(), "/pods/key", obj, false, nil, updateFunc, true, nil) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound { t.Errorf("Expected key to be not found, error: %v", err) } @@ -134,7 +134,7 @@ func TestDryRunUpdatePreconditions(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"uid": "my-uid"}}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } @@ -149,12 +149,12 @@ func TestDryRunUpdatePreconditions(t *testing.T) { } wrongID := types.UID("wrong-uid") myID := types.UID("my-uid") - err = s.GuaranteedUpdate(context.Background(), "key", obj, false, &storage.Preconditions{UID: &wrongID}, updateFunc, true, nil) + err = s.GuaranteedUpdate(context.Background(), "/pods/key", obj, false, &storage.Preconditions{UID: &wrongID}, updateFunc, true, nil) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeInvalidObj { t.Errorf("Expected invalid object, error: %v", err) } - err = s.GuaranteedUpdate(context.Background(), "key", obj, false, &storage.Preconditions{UID: &myID}, updateFunc, true, nil) + err = s.GuaranteedUpdate(context.Background(), "/pods/key", obj, false, &storage.Preconditions{UID: &myID}, updateFunc, true, nil) if err != nil { t.Fatalf("Failed to update with valid precondition: %v", err) } @@ -167,7 +167,7 @@ func TestDryRunUpdateDoesntUpdate(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) created := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, created, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, created, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } @@ -181,12 +181,12 @@ func TestDryRunUpdateDoesntUpdate(t *testing.T) { return u, nil, nil } - err = s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true, nil) + err = s.GuaranteedUpdate(context.Background(), "/pods/key", obj, false, nil, updateFunc, true, nil) if err != nil { t.Fatalf("Failed to dry-run update: %v", err) } out := UnstructuredOrDie(`{}`) - err = s.Get(context.Background(), "key", storage.GetOptions{}, out) + err = s.Get(context.Background(), "/pods/key", storage.GetOptions{}, out) if err != nil { t.Fatalf("Failed to get storage: %v", err) } @@ -202,7 +202,7 @@ func TestDryRunUpdateReturnsObject(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } @@ -216,7 +216,7 @@ func TestDryRunUpdateReturnsObject(t *testing.T) { return u, nil, nil } - err = s.GuaranteedUpdate(context.Background(), "key", obj, false, nil, updateFunc, true, nil) + err = s.GuaranteedUpdate(context.Background(), "/pods/key", obj, false, nil, updateFunc, true, nil) if err != nil { t.Fatalf("Failed to dry-run update: %v", err) } @@ -233,17 +233,17 @@ func TestDryRunDeleteDoesntDelete(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } - err = s.Delete(context.Background(), "key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) + err = s.Delete(context.Background(), "/pods/key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) if err != nil { t.Fatalf("Failed to dry-run delete the object: %v", err) } - err = s.Get(context.Background(), "key", storage.GetOptions{}, out) + err = s.Get(context.Background(), "/pods/key", storage.GetOptions{}, out) if err != nil { t.Fatalf("Failed to retrieve dry-run deleted object: %v", err) } @@ -254,7 +254,7 @@ func TestDryRunDeleteMissingObjectFails(t *testing.T) { defer destroy() out := UnstructuredOrDie(`{}`) - err := s.Delete(context.Background(), "key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) + err := s.Delete(context.Background(), "/pods/key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeKeyNotFound { t.Errorf("Expected key to be not found, error: %v", err) } @@ -267,14 +267,14 @@ func TestDryRunDeleteReturnsObject(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod"}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } out = UnstructuredOrDie(`{}`) expected := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"resourceVersion": "2"}}`) - err = s.Delete(context.Background(), "key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) + err = s.Delete(context.Background(), "/pods/key", out, nil, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) if err != nil { t.Fatalf("Failed to delete with valid precondition: %v", err) } @@ -290,19 +290,19 @@ func TestDryRunDeletePreconditions(t *testing.T) { obj := UnstructuredOrDie(`{"kind": "Pod", "metadata": {"uid": "my-uid"}}`) out := UnstructuredOrDie(`{}`) - err := s.Create(context.Background(), "key", obj, out, 0, false) + err := s.Create(context.Background(), "/pods/key", obj, out, 0, false) if err != nil { t.Fatalf("Failed to create new object: %v", err) } wrongID := types.UID("wrong-uid") myID := types.UID("my-uid") - err = s.Delete(context.Background(), "key", out, &storage.Preconditions{UID: &wrongID}, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) + err = s.Delete(context.Background(), "/pods/key", out, &storage.Preconditions{UID: &wrongID}, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) if e, ok := err.(*storage.StorageError); !ok || e.Code != storage.ErrCodeInvalidObj { t.Errorf("Expected invalid object, error: %v", err) } - err = s.Delete(context.Background(), "key", out, &storage.Preconditions{UID: &myID}, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) + err = s.Delete(context.Background(), "/pods/key", out, &storage.Preconditions{UID: &myID}, rest.ValidateAllObjectFunc, true, nil, storage.DeleteOptions{}) if err != nil { t.Fatalf("Failed to delete with valid precondition: %v", err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/storage_factory.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/storage_factory.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/storage_factory.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/storage_factory.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store.go index 722a31227..4fef3be39 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store.go @@ -27,8 +27,8 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/validate/content" "k8s.io/apimachinery/pkg/api/validation" - "k8s.io/apimachinery/pkg/api/validation/path" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -45,6 +45,7 @@ import ( "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/sharding" "k8s.io/apiserver/pkg/storage" storeerr "k8s.io/apiserver/pkg/storage/errors" "k8s.io/apiserver/pkg/storage/etcd3/metrics" @@ -286,7 +287,7 @@ func NamespaceKeyFunc(ctx context.Context, prefix string, name string) (string, if len(name) == 0 { return "", apierrors.NewBadRequest("Name parameter required.") } - if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 { + if msgs := content.IsPathSegmentName(name); len(msgs) != 0 { return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";"))) } key = key + "/" + name @@ -299,7 +300,7 @@ func NoNamespaceKeyFunc(ctx context.Context, prefix string, name string) (string if len(name) == 0 { return "", apierrors.NewBadRequest("Name parameter required.") } - if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 { + if msgs := content.IsPathSegmentName(name); len(msgs) != 0 { return "", apierrors.NewBadRequest(fmt.Sprintf("Name parameter invalid: %q: %s", name, strings.Join(msgs, ";"))) } key := prefix + "/" + name @@ -393,6 +394,13 @@ func (e *Store) ListPredicate(ctx context.Context, p storage.SelectionPredicate, } p.Limit = options.Limit p.Continue = options.Continue + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) && options.ShardSelector != "" { + sel, err := sharding.Parse(options.ShardSelector) + if err != nil { + return nil, fmt.Errorf("invalid shard selector: %w", err) + } + p.ShardSelector = sel + } list := e.NewListFunc() qualifiedResource := e.qualifiedResourceFromContext(ctx) storageOpts := storage.ListOptions{ @@ -1263,7 +1271,7 @@ func (e *Store) DeleteCollection(ctx context.Context, deleteValidation rest.Vali for i := 0; i < workersNumber; i++ { go func() { // panics don't cross goroutine boundaries - defer utilruntime.HandleCrash(func(panicReason interface{}) { + defer utilruntime.HandleCrashWithContext(ctx, func(_ context.Context, panicReason interface{}) { errs <- fmt.Errorf("DeleteCollection goroutine panicked: %v", panicReason) }) defer wg.Done() @@ -1288,7 +1296,7 @@ func (e *Store) DeleteCollection(ctx context.Context, deleteValidation rest.Vali } // In case of all workers exit, notify distributor. go func() { - defer utilruntime.HandleCrash(func(panicReason interface{}) { + defer utilruntime.HandleCrashWithContext(ctx, func(_ context.Context, panicReason interface{}) { errs <- fmt.Errorf("DeleteCollection workers closer panicked: %v", panicReason) }) wg.Wait() @@ -1429,13 +1437,25 @@ func (e *Store) Watch(ctx context.Context, options *metainternalversion.ListOpti if options != nil { resourceVersion = options.ResourceVersion predicate.AllowWatchBookmarks = options.AllowWatchBookmarks + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) && options.ShardSelector != "" { + sel, err := sharding.Parse(options.ShardSelector) + if err != nil { + return nil, fmt.Errorf("invalid shard selector: %w", err) + } + predicate.ShardSelector = sel + } } return e.WatchPredicate(ctx, predicate, resourceVersion, options.SendInitialEvents) } // WatchPredicate starts a watch for the items that matches. func (e *Store) WatchPredicate(ctx context.Context, p storage.SelectionPredicate, resourceVersion string, sendInitialEvents *bool) (watch.Interface, error) { - storageOpts := storage.ListOptions{ResourceVersion: resourceVersion, Predicate: p, Recursive: true, SendInitialEvents: sendInitialEvents} + storageOpts := storage.ListOptions{ + ResourceVersion: resourceVersion, + Predicate: p, + Recursive: true, + SendInitialEvents: sendInitialEvents, + } // if we're not already namespace-scoped, see if the field selector narrows the scope of the watch if requestNamespace, _ := genericapirequest.NamespaceFrom(ctx); len(requestNamespace) == 0 { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store_test.go index cc50afede..bb90542a4 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/registry/store_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/registry/store_test.go @@ -2427,7 +2427,7 @@ func TestStoreWatch(t *testing.T) { } func newTestGenericStoreRegistry(t *testing.T, scheme *runtime.Scheme, hasCacheEnabled bool) (factory.DestroyFunc, *Store) { - podPrefix := "/pods" + podPrefix := "/pods/" server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t) strategy := &testRESTStrategy{scheme, names.SimpleNameGenerator, true, false, true} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/response_checker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/response_checker.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/response_checker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/response_checker.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/response_checker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/response_checker_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/response_checker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/response_checker_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/streamer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/streamer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/streamer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/streamer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/streamer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/streamer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/rest/streamer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/rest/streamer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/storage_decorator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/storage_decorator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/storage_decorator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/storage_decorator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/testing/tester.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/testing/tester.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/testing/tester.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/testing/tester.go index dc2172ede..14bcac419 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/generic/testing/tester.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/generic/testing/tester.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/authentication/user" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest/resttest" @@ -80,6 +81,11 @@ func (t *Tester) ReturnDeletedObject() *Tester { return t } +func (t *Tester) SetRequestInfo(requestInfo *genericapirequest.RequestInfo) *Tester { + t.tester.SetRequestInfo(requestInfo) + return t +} + func (t *Tester) TestCreate(valid runtime.Object, invalid ...runtime.Object) { t.tester.TestCreate( valid, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/create.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/create.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/create.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/create.go index b3e57d0a4..87b2c9d81 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/create.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/create.go @@ -22,8 +22,8 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/validate/content" genericvalidation "k8s.io/apimachinery/pkg/api/validation" - "k8s.io/apimachinery/pkg/api/validation/path" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -89,6 +89,13 @@ type RESTCreateStrategy interface { Canonicalize(obj runtime.Object) } +func validatePathSegment(name string, prefix bool) []string { + if prefix { + return content.IsPathSegmentPrefix(name) + } + return content.IsPathSegmentName(name) +} + // BeforeCreate ensures that common operations for all resources are performed on creation. It only returns // errors that can be converted to api.Status. It invokes PrepareForCreate, then Validate. // It returns nil if the object should be created. @@ -126,7 +133,7 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime. // Custom validation (including name validation) passed // Now run common validation on object meta // Do this *after* custom validation so that specific error messages are shown whenever possible - if errs := genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), path.ValidatePathSegmentName, field.NewPath("metadata")); len(errs) > 0 { + if errs := genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), validatePathSegment, field.NewPath("metadata")); len(errs) > 0 { return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/create_update.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/create_update.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/create_update.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/create_update.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/delete.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/delete.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/delete.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/delete.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/delete_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/delete_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/delete_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/delete_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/meta.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/meta.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/meta.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/meta.go index fc4fc81e1..47f7f83b7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/meta.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/meta.go @@ -24,7 +24,7 @@ import ( ) // metav1Now returns metav1.Now(), but allows override for unit testing -var metav1Now = func() metav1.Time { return metav1.Now() } +var metav1Now = metav1.Now // WipeObjectMetaSystemFields erases fields that are managed by the system on ObjectMeta. func WipeObjectMetaSystemFields(meta metav1.Object) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/meta_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/meta_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/meta_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/meta_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/rest.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/rest.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/rest.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/rest.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/resttest/resttest.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/resttest/resttest.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/resttest/resttest.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/resttest/resttest.go index 000bb5249..e003cfe13 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/resttest/resttest.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/resttest/resttest.go @@ -28,7 +28,6 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/api/validation/path" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" @@ -54,6 +53,7 @@ type Tester struct { returnDeletedObject bool namer func(int) string userInfo user.Info + requestInfo *genericapirequest.RequestInfo } func New(t *testing.T, storage rest.Storage) *Tester { @@ -95,6 +95,12 @@ func (t *Tester) ReturnDeletedObject() *Tester { return t } +// SetRequestInfo sets the RequestInfo that should be present in the context when the +// storage operation is called. +func (t *Tester) SetRequestInfo(requestInfo *genericapirequest.RequestInfo) { + t.requestInfo = requestInfo +} + // TestNamespace returns the namespace that will be used when creating contexts. // Returns NamespaceNone for cluster-scoped objects. func (t *Tester) TestNamespace() string { @@ -118,6 +124,9 @@ func (t *Tester) TestContext() context.Context { if t.userInfo != nil { ctx = genericapirequest.WithUser(ctx, t.userInfo) } + if t.requestInfo != nil { + ctx = genericapirequest.WithRequestInfo(ctx, t.requestInfo) + } return ctx } @@ -416,6 +425,9 @@ func (t *Tester) testCreateHasMetadata(valid runtime.Object) { func (t *Tester) testCreateIgnoresContextNamespace(valid runtime.Object, opts metav1.CreateOptions) { // Ignore non-empty namespace in context ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), "not-default2") + if t.requestInfo != nil { + ctx = genericapirequest.WithRequestInfo(ctx, t.requestInfo) + } // Ideally, we'd get an error back here, but at least verify the namespace wasn't persisted created, err := t.storage.(rest.Creater).Create(ctx, valid.DeepCopyObject(), rest.ValidateAllObjectFunc, &opts) @@ -435,6 +447,9 @@ func (t *Tester) testCreateIgnoresMismatchedNamespace(valid runtime.Object, opts // Ignore non-empty namespace in object meta objectMeta.SetNamespace("not-default") ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), "not-default2") + if t.requestInfo != nil { + ctx = genericapirequest.WithRequestInfo(ctx, t.requestInfo) + } // Ideally, we'd get an error back here, but at least verify the namespace wasn't persisted created, err := t.storage.(rest.Creater).Create(ctx, valid.DeepCopyObject(), rest.ValidateAllObjectFunc, &opts) @@ -449,7 +464,8 @@ func (t *Tester) testCreateIgnoresMismatchedNamespace(valid runtime.Object, opts } func (t *Tester) testCreateValidatesNames(valid runtime.Object, opts metav1.CreateOptions) { - for _, invalidName := range path.NameMayNotBe { + invalidPathElements := []string{".", ".."} + for _, invalidName := range invalidPathElements { objCopy := valid.DeepCopyObject() objCopyMeta := t.getObjectMetaOrFail(objCopy) objCopyMeta.SetName(invalidName) @@ -461,7 +477,8 @@ func (t *Tester) testCreateValidatesNames(valid runtime.Object, opts metav1.Crea } } - for _, invalidSuffix := range path.NameMayNotContain { + invalidInPath := []string{"/", "%"} + for _, invalidSuffix := range invalidInPath { objCopy := valid.DeepCopyObject() objCopyMeta := t.getObjectMetaOrFail(objCopy) objCopyMeta.SetName(objCopyMeta.GetName() + invalidSuffix) @@ -1178,6 +1195,9 @@ func (t *Tester) testGetDifferentNamespace(obj runtime.Object) { objMeta.SetName(t.namer(5)) ctx1 := genericapirequest.WithNamespace(genericapirequest.NewContext(), "bar3") + if t.requestInfo != nil { + ctx1 = genericapirequest.WithRequestInfo(ctx1, t.requestInfo) + } objMeta.SetNamespace(genericapirequest.NamespaceValue(ctx1)) _, err := t.storage.(rest.Creater).Create(ctx1, obj, rest.ValidateAllObjectFunc, &metav1.CreateOptions{}) if err != nil { @@ -1185,6 +1205,9 @@ func (t *Tester) testGetDifferentNamespace(obj runtime.Object) { } ctx2 := genericapirequest.WithNamespace(genericapirequest.NewContext(), "bar4") + if t.requestInfo != nil { + ctx2 = genericapirequest.WithRequestInfo(ctx2, t.requestInfo) + } objMeta.SetNamespace(genericapirequest.NamespaceValue(ctx2)) _, err = t.storage.(rest.Creater).Create(ctx2, obj, rest.ValidateAllObjectFunc, &metav1.CreateOptions{}) if err != nil { @@ -1239,7 +1262,13 @@ func (t *Tester) testGetFound(obj runtime.Object) { func (t *Tester) testGetMimatchedNamespace(obj runtime.Object) { ctx1 := genericapirequest.WithNamespace(genericapirequest.NewContext(), "bar1") + if t.requestInfo != nil { + ctx1 = genericapirequest.WithRequestInfo(ctx1, t.requestInfo) + } ctx2 := genericapirequest.WithNamespace(genericapirequest.NewContext(), "bar2") + if t.requestInfo != nil { + ctx2 = genericapirequest.WithRequestInfo(ctx2, t.requestInfo) + } objMeta := t.getObjectMetaOrFail(obj) objMeta.SetName(t.namer(4)) objMeta.SetNamespace(genericapirequest.NamespaceValue(ctx1)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/table.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/table.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/table.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/table.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/update.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/update.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/update.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/update.go index dc63caf0b..32de351aa 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/registry/rest/update.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/update.go @@ -23,7 +23,6 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" genericvalidation "k8s.io/apimachinery/pkg/api/validation" - "k8s.io/apimachinery/pkg/api/validation/path" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -94,7 +93,7 @@ func validateCommonFields(obj, old runtime.Object, strategy RESTUpdateStrategy) if err != nil { return nil, fmt.Errorf("failed to get old object metadata: %v", err) } - allErrs = append(allErrs, genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), path.ValidatePathSegmentName, field.NewPath("metadata"))...) + allErrs = append(allErrs, genericvalidation.ValidateObjectMetaAccessor(objectMeta, strategy.NamespaceScoped(), validatePathSegment, field.NewPath("metadata"))...) allErrs = append(allErrs, genericvalidation.ValidateObjectMetaAccessorUpdate(objectMeta, oldObjectMeta, field.NewPath("metadata"))...) return allErrs, nil @@ -153,6 +152,7 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx context.Context, obj, old run errs = append(errs, strategy.ValidateUpdate(ctx, obj, old)...) if len(errs) > 0 { + RecordDuplicateValidationErrors(ctx, kind.GroupKind(), errs) return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs) } diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate.go new file mode 100644 index 000000000..f0e355e54 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate.go @@ -0,0 +1,507 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/api/operation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + validationmetrics "k8s.io/apiserver/pkg/validation" + "k8s.io/klog/v2" +) + +// ValidationConfig defines how a declarative validation request may be configured. +type ValidationConfig func(*validationConfigOption) + +// WithOptions sets the validation options. +// Options should contain any validation options that the declarative validation +// tags expect. These often correspond to feature gates. +func WithOptions(options []string) ValidationConfig { + return func(config *validationConfigOption) { + config.options = options + } +} + +// WithSubresourceMapper sets the subresource mapper for validation. +// This should be used when registering validation for polymorphic subresources like /scale. +// +// For example, the deployments/scale subresource mapper might map from: +// +// group: apps, version: v1, subresource=scale +// +// to a target of: +// +// group: autoscaling, version: v1, kind=Scale +// +// When set, the group version in the requestInfo of the ctx provided to a declarative validation +// request will be passed to the subresource mapper to find the group version kind of the subresource. +// Declarative validation will then convert the object to the subresource group version kind and validate it. +// +// Note that the target of the mapping contains no subresource part since the mapper is expected to +// map to the group version kind of the subresource. +func WithSubresourceMapper(subresourceMapper GroupVersionKindProvider) ValidationConfig { + return func(config *validationConfigOption) { + config.subresourceGVKMapper = subresourceMapper + } +} + +// WithNormalizationRules sets the normalization rules for validation. +func WithNormalizationRules(rules []field.NormalizationRule) ValidationConfig { + return func(config *validationConfigOption) { + config.normalizationRules = rules + } +} + +// WithDeclarativeEnforcement marks the validation configuration to indicate that it includes +// declarative validations that should follow the fine-grained Validation Lifecycle. +// When set, declarative validation is always executed regardless of feature gates. +// Authority is determined by individual tag prefixes (+k8s:alpha, +k8s:beta) and the +// DeclarativeValidationBeta safety switch. +func WithDeclarativeEnforcement() ValidationConfig { + return func(config *validationConfigOption) { + config.declarativeEnforcement = true + } +} + +type allDeclarativeEnforcedKeyType struct{} + +var allDeclarativeEnforcedKey = allDeclarativeEnforcedKeyType{} + +// WithAllDeclarativeEnforcedForTest returns a copy of parent context with allDeclarativeEnforcedKey set to true. +// This is used for testing to expose all declarative validation errors and filter all handwritten validation errors +// that are covered by declarative validation, regardless of the feature gate or maturity level. +// +// NOTE: This function is intended for testing purposes only and should not be used in production code. +func WithAllDeclarativeEnforcedForTest(ctx context.Context) context.Context { + return context.WithValue(ctx, allDeclarativeEnforcedKey, true) +} + +type validationConfigOption struct { + opType operation.Type + options []string + subresourceGVKMapper GroupVersionKindProvider + validationIdentifier string + normalizationRules []field.NormalizationRule + declarativeEnforcement bool +} + +// validateDeclaratively validates obj and oldObj against declarative +// validation tags defined in its Go type. It uses the API version extracted from +// ctx and the provided scheme for validation. +// +// The ctx MUST contain requestInfo, which determines the target API for +// validation. The obj is converted to the API version using the provided scheme +// before validation occurs. The scheme MUST have the declarative validation +// registered for the requested resource/subresource. +// +// Returns a field.ErrorList containing any validation errors. An internal error +// is included if requestInfo is missing from the context or if version +// conversion fails. +func validateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList { + // Find versionedGroupVersion, which identifies the API version to use for declarative validation. + versionedGroupVersion, subresources, err := requestInfo(ctx, o.subresourceGVKMapper) + if err != nil { + return field.ErrorList{field.InternalError(nil, err)} + } + versionedObj, err := scheme.ConvertToVersion(obj, versionedGroupVersion) + if err != nil { + return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))} + } + var versionedOldObj runtime.Object + + switch o.opType { + case operation.Create: + return scheme.Validate(ctx, o.options, versionedObj, subresources...) + case operation.Update: + versionedOldObj, err = scheme.ConvertToVersion(oldObj, versionedGroupVersion) + if err != nil { + return field.ErrorList{field.InternalError(nil, fmt.Errorf("unexpected error converting to versioned type: %w", err))} + } + return scheme.ValidateUpdate(ctx, o.options, versionedObj, versionedOldObj, subresources...) + default: + return field.ErrorList{field.InternalError(nil, fmt.Errorf("unknown operation type: %v", o.opType))} + } +} + +func requestInfo(ctx context.Context, subresourceMapper GroupVersionKindProvider) (schema.GroupVersion, []string, error) { + requestInfo, found := genericapirequest.RequestInfoFrom(ctx) + if !found { + return schema.GroupVersion{}, nil, fmt.Errorf("could not find requestInfo in context") + } + groupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} + if subresourceMapper != nil { + groupVersion = subresourceMapper.GroupVersionKind(groupVersion).GroupVersion() + } + subresources, err := parseSubresourcePath(requestInfo.Subresource) + if err != nil { + return schema.GroupVersion{}, nil, fmt.Errorf("unexpected error parsing subresource path: %w", err) + } + return groupVersion, subresources, nil + +} + +func parseSubresourcePath(subresourcePath string) ([]string, error) { + if len(subresourcePath) == 0 { + return nil, nil + } + parts := strings.Split(subresourcePath, "/") + for _, part := range parts { + if len(part) == 0 { + return nil, fmt.Errorf("invalid subresource path: %s", subresourcePath) + } + } + return parts, nil +} + +// compareDeclarativeErrorsAndEmitMismatches checks for mismatches between imperative and declarative validation +// and logs + emits metrics when inconsistencies are found +func compareDeclarativeErrorsAndEmitMismatches(ctx context.Context, imperativeErrs, declarativeErrs field.ErrorList, enforced bool, validationIdentifier string, normalizationRules []field.NormalizationRule) { + logger := klog.FromContext(ctx) + mismatchDetails := gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs, enforced, normalizationRules) + for _, detail := range mismatchDetails { + // Log information about the mismatch using contextual logger + logger.Error(nil, detail) + + // Increment the metric for the mismatch + validationmetrics.Metrics.IncDeclarativeValidationMismatchMetric(validationIdentifier) + } +} + +// gatherDeclarativeValidationMismatches compares imperative and declarative validation errors +// and returns detailed information about any mismatches found. Errors are compared via type, field, and origin +func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field.ErrorList, enforced bool, normalizationRules []field.NormalizationRule) []string { + var mismatchDetails []string + // short circuit here to minimize allocs for usual case of 0 validation errors + if len(imperativeErrs) == 0 && len(declarativeErrs) == 0 { + return mismatchDetails + } + // default recommendation based on enforcement status + const ( + authoritativeMsg = "This difference should not affect system operation since hand written validation is authoritative." + disableBetaMsg = "Consider disabling the DeclarativeValidationBeta feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes." + ) + + defaultRecommendation := authoritativeMsg + if enforced { + defaultRecommendation = disableBetaMsg + } + + fuzzyMatcher := field.ErrorMatcher{}.ByType().ByOrigin().RequireOriginWhenInvalid().ByFieldNormalized(normalizationRules) + + // Dedupe imperative errors using the fuzzy matcher (type, field, and origin) as they are + // not intended and come from (buggy) duplicate validation calls. + // This is necessary as without deduping we could get unmatched + // imperative errors for cases that are correct (matching). + dedupedImperativeErrs := field.ErrorList{} + for _, err := range imperativeErrs { + found := false + for _, existingErr := range dedupedImperativeErrs { + if fuzzyMatcher.Matches(existingErr, err) { + found = true + break + } + } + if !found { + dedupedImperativeErrs = append(dedupedImperativeErrs, err) + } + } + imperativeErrs = dedupedImperativeErrs + + // Create a copy of declarative errors to track remaining ones + remaining := make(field.ErrorList, len(declarativeErrs)) + copy(remaining, declarativeErrs) + + // Match each "covered" imperative error to declarative errors. + // We use a fuzzy matching approach to find corresponding declarative errors + // for each imperative error marked as CoveredByDeclarative. + // As matches are found, they're removed from the 'remaining' list. + // They are removed from `remaining` with a "1:many" mapping: for a given + // imperative error we mark as matched all matching declarative errors + // This allows us to: + // 1. Detect imperative errors that should have matching declarative errors but don't + // 2. Identify extra declarative errors with no imperative counterpart + // Both cases indicate issues with the declarative validation implementation. + for _, iErr := range imperativeErrs { + if !iErr.CoveredByDeclarative { + continue + } + + tmp := make(field.ErrorList, 0, len(remaining)) + matchCount := 0 + + for _, dErr := range remaining { + if fuzzyMatcher.Matches(iErr, dErr) { + matchCount++ + } else { + tmp = append(tmp, dErr) + } + } + + if matchCount == 0 { + rec := defaultRecommendation + // If the imperative error is explicitly Alpha, it is never enforced, so HV is authoritative. + if iErr.IsAlpha() { + rec = authoritativeMsg + } + + mismatchDetails = append(mismatchDetails, + fmt.Sprintf( + "Unexpected difference between hand written validation and declarative validation error results, unmatched error(s) found %s. "+ + "This indicates an issue with declarative validation. %s", + fuzzyMatcher.Render(iErr), + rec, + ), + ) + } + + remaining = tmp + } + + // Any remaining unmatched declarative errors are considered "extra" + for _, dErr := range remaining { + rec := defaultRecommendation + // If the declarative error is Alpha, it is never enforced (shadowed), so HV is authoritative. + if dErr.IsAlpha() { + rec = authoritativeMsg + } + + mismatchDetails = append(mismatchDetails, + fmt.Sprintf( + "Unexpected difference between hand written validation and declarative validation error results, extra error(s) found %s. "+ + "This indicates an issue with declarative validation. %s", + fuzzyMatcher.Render(dErr), + rec, + ), + ) + } + + return mismatchDetails +} + +// createDeclarativeValidationPanicHandler returns a function with panic recovery logic +// that will increment the panic metric and either log or append errors based on the shouldFail parameter. +func createDeclarativeValidationPanicHandler(ctx context.Context, errs *field.ErrorList, shouldFail bool, validationIdentifier string) func() { + logger := klog.FromContext(ctx) + return func() { + if r := recover(); r != nil { + // Increment the panic metric counter + validationmetrics.Metrics.IncDeclarativeValidationPanicMetric(validationIdentifier) + + const errorFmt = "panic during declarative validation: %v" + if shouldFail { + // If shouldFail is enabled, output as a validation error as authoritative validator panicked and validation should error + *errs = append(*errs, field.InternalError(nil, fmt.Errorf(errorFmt, r))) + } else { + // if shouldFail not enabled, log the panic as an error message + logger.Error(nil, fmt.Sprintf(errorFmt, r)) + } + } + } +} + +// panicSafeValidateFunc wraps an validation function with panic recovery logic. +// The returned function will execute the wrapped function and handle any panics by +// incrementing the panic metric, and logging an error message +// if shouldFail=false, and adding a validation error if shouldFail=true. +func panicSafeValidateFunc( + validateFunc func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList, + shouldFail bool, validationIdentifier string, +) func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList { + return func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) (errs field.ErrorList) { + defer createDeclarativeValidationPanicHandler(ctx, &errs, shouldFail, validationIdentifier)() + + return validateFunc(ctx, scheme, obj, oldObj, o) + } +} + +func metricIdentifier(ctx context.Context, scheme *runtime.Scheme, obj runtime.Object, opType operation.Type) (string, error) { + var errs error + var identifier string + + identifier = "unknown_resource" + // Use kind for identifier. + if obj != nil && scheme != nil { + gvks, _, err := scheme.ObjectKinds(obj) + if err != nil { + errs = errors.Join(errs, err) + } + if len(gvks) > 0 { + identifier = strings.ToLower(gvks[0].Kind) + } + } + + // Use requestInfo for subresource. + requestInfo, found := genericapirequest.RequestInfoFrom(ctx) + if !found { + errs = errors.Join(errs, fmt.Errorf("could not find requestInfo in context")) + } else if len(requestInfo.Subresource) > 0 { + // subresource can be a path, so replace '/' with '_' + identifier += "_" + strings.ReplaceAll(requestInfo.Subresource, "/", "_") + } + + switch opType { + case operation.Create: + identifier += "_create" + case operation.Update: + identifier += "_update" + default: + errs = errors.Join(errs, fmt.Errorf("unknown operation type: %v", opType)) + identifier += "_unknown_op" + } + return identifier, errs +} + +// ValidateDeclarativelyWithMigrationChecks executes declarative validation and implements the Validation Lifecycle strategy. +// It manages the transition from handwritten (HV) to declarative (DV) validation by controlling enforcement: +// - Standard: Enforced if declarativeEnforcement is set. HV counterparts are expected to be deleted from source. +// - Beta: Enforced if declarativeEnforcement is set AND DeclarativeValidationBeta feature gate is enabled. +// When enforced, corresponding HV errors are filtered out. Otherwise, DV is shadowed. +// - Alpha: Always shadowed; HV remains authoritative. +// +// Mismatches between HV and DV are logged if the DeclarativeValidation gate is enabled. +// Mismatch checking is limited to Alpha and Beta stages when explicit enforcement is active. +// +// For testing purposes, WithAllDeclarativeEnforcedForTest can be used to enforce all declarative validations +// regardless of feature gates and filter all covered handwritten validations. +func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, errs field.ErrorList, opType operation.Type, configOpts ...ValidationConfig) field.ErrorList { + declarativeValidationEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) + betaEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationBeta) + // allDeclarativeEnforced indicates that we should check all declarative errors for testing purposes. + allDeclarativeEnforced := ctx.Value(allDeclarativeEnforcedKey) == true + // These errors must be errors returned by the handwritten validation. + errs = errs.MarkFromImperative() + validationIdentifier, err := metricIdentifier(ctx, scheme, obj, opType) + if err != nil { + // Log the error, but continue with the best-effort identifier. + klog.FromContext(ctx).Error(err, "failed to generate complete validation identifier for declarative validation") + } + + // Directly create the config and call the core validation logic. + cfg := &validationConfigOption{ + opType: opType, + validationIdentifier: validationIdentifier, + } + for _, opt := range configOpts { + opt(cfg) + } + + // Short-circuit if neither DeclarativeValidation is enabled nor the object is explicitly configured for declarative enforcement. + if !declarativeValidationEnabled && !cfg.declarativeEnforcement && !allDeclarativeEnforced { + return errs + } + + // Call the panic-safe wrapper with the real validation function. + // We should fail if validation is enforced. + declarativeErrs := panicSafeValidateFunc(validateDeclaratively, cfg.declarativeEnforcement, cfg.validationIdentifier)(ctx, scheme, obj, oldObj, cfg) + + if declarativeValidationEnabled { + // Log mismatches. + // When explicit strategy is used (declarativeEnforcement), Standard errors are authoritative + // and may not have handwritten counterparts (e.g., in new APIs). + // We only mismatch check Alpha and Beta errors in this mode. + mismatchCandidateErrs := declarativeErrs + if cfg.declarativeEnforcement { + mismatchCandidateErrs = nil + for _, err := range declarativeErrs { + if err.IsAlpha() || err.IsBeta() { + mismatchCandidateErrs = append(mismatchCandidateErrs, err) + } + } + } + + // We pass betaEnabled (and enforcement) as the takeover flag to avoid changing logic elsewhere for now. + compareDeclarativeErrorsAndEmitMismatches(ctx, errs, mismatchCandidateErrs, cfg.declarativeEnforcement && betaEnabled, validationIdentifier, cfg.normalizationRules) + } + + if !cfg.declarativeEnforcement && !allDeclarativeEnforced { + // If enforcement is not enabled, we shadow declarative errors with hand-written ones, so we return early here. + return errs + } + + // Filter HV errors + errs = filterHandwrittenErrors(errs, allDeclarativeEnforced, betaEnabled) + + // Append Enforced DV errors + for _, dvErr := range declarativeErrs { + if allDeclarativeEnforced { + errs = append(errs, dvErr) + continue + } + switch { + case dvErr.Type == field.ErrorTypeInternal: + errs = append(errs, dvErr) + case dvErr.IsBeta(): + if betaEnabled { + errs = append(errs, dvErr) + } + case !dvErr.IsAlpha(): + errs = append(errs, dvErr) // Standard + } + } + + return errs +} + +func filterHandwrittenErrors(errs field.ErrorList, allDeclarativeEnforced, betaEnabled bool) field.ErrorList { + // We remove HV errors that are covered by declarative validation AND are enforced. + return errs.Filter(func(e error) bool { + var fe *field.Error + if !errors.As(e, &fe) || !fe.CoveredByDeclarative { + return false + } + + if allDeclarativeEnforced { + return true + } + + // Explicit Strategy + if fe.IsBeta() { + // Beta validations are enforced only if the Beta feature gate is enabled. + return betaEnabled + } + // For Standard validations, we keep the handwritten error for now to avoid losing coverage + // before it is deleted from source. Alpha validations are always shadowed (kept). + return false + }) +} + +// RecordDuplicateValidationErrors increments a metric and log the error when duplicate validation errors are found. +func RecordDuplicateValidationErrors(ctx context.Context, qualifiedKind schema.GroupKind, errs field.ErrorList) { + logger := klog.FromContext(ctx) + seenErrs := make([]string, 0, len(errs)) + + for _, err := range errs { + errStr := fmt.Sprintf("%v", err) + + if slices.Contains(seenErrs, errStr) { + logger.Info("Found duplicate validation error", "kind", qualifiedKind.String(), "error", errStr) + validationmetrics.Metrics.IncDuplicateValidationErrorMetric() + } else { + seenErrs = append(seenErrs, errStr) + } + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate_test.go new file mode 100644 index 000000000..51f529514 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/registry/rest/validate_test.go @@ -0,0 +1,947 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest + +import ( + "bytes" + "context" + "fmt" + "reflect" + "regexp" + "slices" + "strings" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/operation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/version" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/pkg/validation" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" + "k8s.io/klog/v2" +) + +func TestValidateDeclaratively(t *testing.T) { + valid := &Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + } + + invalidRestartPolicy := &Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + RestartPolicy: "INVALID", + } + + invalidRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid value").WithOrigin("invalid-test") + mutatedRestartPolicyErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Immutable field").WithOrigin("immutable-test") + invalidStatusErr := field.Invalid(field.NewPath("status", "conditions"), "", "Invalid condition").WithOrigin("invalid-condition") + invalidIfOptionErr := field.Invalid(field.NewPath("spec", "restartPolicy"), "", "Invalid when option is set").WithOrigin("invalid-when-option-set") + invalidSubresourceErr := field.InternalError(nil, fmt.Errorf("unexpected error parsing subresource path: %w", fmt.Errorf("invalid subresource path: %s", "invalid/status"))) + + testCases := []struct { + name string + object runtime.Object + oldObject runtime.Object + subresource string + options []string + expected field.ErrorList + }{ + { + name: "create", + object: invalidRestartPolicy, + expected: field.ErrorList{invalidRestartPolicyErr}, + }, + { + name: "update", + object: invalidRestartPolicy, + oldObject: valid, + expected: field.ErrorList{invalidRestartPolicyErr, mutatedRestartPolicyErr}, + }, + { + name: "update subresource with declarative validation", + subresource: "status", + object: valid, + oldObject: valid, + expected: field.ErrorList{invalidStatusErr}, + }, + { + name: "update subresource without declarative validation", + subresource: "scale", + object: valid, + oldObject: valid, + expected: field.ErrorList{}, // Expect no errors if there is no registered validation + }, + { + name: "invalid subresource", + subresource: "/invalid/status", + object: valid, + oldObject: valid, + expected: field.ErrorList{invalidSubresourceErr}, + }, + { + name: "update with option", + options: []string{"option1"}, + object: valid, + expected: field.ErrorList{invalidIfOptionErr}, + }, + } + + ctx := context.Background() + + internalGV := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal} + v1GV := schema.GroupVersion{Group: "", Version: "v1"} + + scheme := runtime.NewScheme() + scheme.AddKnownTypes(internalGV, &Pod{}) + scheme.AddKnownTypes(v1GV, &v1.Pod{}) + + scheme.AddValidationFunc(&v1.Pod{}, func(ctx context.Context, op operation.Operation, object, oldObject any) field.ErrorList { + results := field.ErrorList{} + if op.HasOption("option1") { + results = append(results, invalidIfOptionErr) + } + if slices.Equal(op.Request.Subresources, []string{"status"}) { + results = append(results, invalidStatusErr) + } + if op.Type == operation.Update && object.(*v1.Pod).Spec.RestartPolicy != oldObject.(*v1.Pod).Spec.RestartPolicy { + results = append(results, mutatedRestartPolicyErr) + } + if object.(*v1.Pod).Spec.RestartPolicy == "INVALID" { + results = append(results, invalidRestartPolicyErr) + } + return results + }) + err := scheme.AddConversionFunc(&Pod{}, &v1.Pod{}, func(a, b interface{}, scope conversion.Scope) error { + if in, ok := a.(*Pod); ok { + if out, ok := b.(*v1.Pod); ok { + out.APIVersion = in.APIVersion + out.Kind = in.Kind + out.Spec.RestartPolicy = v1.RestartPolicy(in.RestartPolicy) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + + for _, tc := range testCases { + ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{ + APIGroup: "", + APIVersion: "v1", + Subresource: tc.subresource, + }) + t.Run(tc.name, func(t *testing.T) { + + cfg := &validationConfigOption{ + options: tc.options, + } + if tc.oldObject == nil { + cfg.opType = operation.Create + } else { + cfg.opType = operation.Update + } + // takeover is not used here, passing false for shouldFail + results := panicSafeValidateFunc(validateDeclaratively, false, cfg.validationIdentifier)(ctx, scheme, tc.object, tc.oldObject, cfg) + matcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin() + matcher.Test(t, tc.expected, results) + }) + } +} + +// Fake internal pod type, since core.Pod cannot be imported by this package +type Pod struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + RestartPolicy string `json:"restartPolicy"` +} + +func (Pod) GetObjectKind() schema.ObjectKind { return schema.EmptyObjectKind } + +func (p Pod) DeepCopyObject() runtime.Object { + return &Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: p.APIVersion, + Kind: p.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: p.Name, + Namespace: p.Namespace, + }, + RestartPolicy: p.RestartPolicy, + } +} + +// TestGatherDeclarativeValidationMismatches tests all mismatch +// scenarios across imperative and declarative errors for +// the gatherDeclarativeValidationMismatches function +func TestGatherDeclarativeValidationMismatches(t *testing.T) { + pathStandard := field.NewPath("spec", "standard") + pathBeta := field.NewPath("spec", "beta") + pathAlpha := field.NewPath("spec", "alpha") + pathOther := field.NewPath("spec", "other") + + // Standard Errors + errHVStandard := field.Invalid(pathStandard, "val", "impStandard").MarkCoveredByDeclarative().WithOrigin("min") + errDVStandard := field.Invalid(pathStandard, "val", "decStandard").WithOrigin("min") + + // Beta Errors + errHVBeta := field.Invalid(pathBeta, "val", "impBeta").MarkCoveredByDeclarative().MarkBeta().WithOrigin("min") + + // Alpha Errors + errDVAlpha := field.Invalid(pathAlpha, "val", "decAlpha").MarkAlpha().WithOrigin("min") + // Note: No HV Alpha by convention, so if it exists it would be a mismatch if DV is missing. + // But we usually don't mark HV as covered for Alpha rules. + + // Normalization / Fuzzy match helpers + errDVStandardDiffDetail := field.Invalid(pathStandard, "val", "decStandardDiffDetail").WithOrigin("min") + errDVStandardDiffPath := field.Invalid(field.NewPath("spec", "standardAliased"), "val", "decStandard").WithOrigin("min") + + const ( + authoritativeMsg = "This difference should not affect system operation since hand written validation is authoritative." + disableBetaMsg = "Consider disabling the DeclarativeValidationBeta feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes." + ) + + testCases := []struct { + name string + imperativeErrors field.ErrorList + declarativeErrors field.ErrorList + enforced bool + expectMismatches bool + expectDetailsContaining []string + normalizedRules []field.NormalizationRule + }{ + { + name: "No errors - no mismatch", + imperativeErrors: field.ErrorList{}, + declarativeErrors: field.ErrorList{}, + expectMismatches: false, + }, + { + name: "Clean match - no mismatch", + imperativeErrors: field.ErrorList{errHVStandard}, + declarativeErrors: field.ErrorList{errDVStandard}, + expectMismatches: false, + }, + { + name: "Mismatch: Missing DV (Standard) + Enforced -> Disable Beta Gate", + imperativeErrors: field.ErrorList{errHVStandard}, + declarativeErrors: field.ErrorList{}, + enforced: true, + expectMismatches: true, + expectDetailsContaining: []string{ + "unmatched error(s) found", + "spec.standard", + disableBetaMsg, + }, + }, + { + name: "Mismatch: Extra DV (Standard) + Not Enforced -> HV Authoritative", + imperativeErrors: field.ErrorList{}, + declarativeErrors: field.ErrorList{errDVStandard}, + enforced: false, + expectMismatches: true, + expectDetailsContaining: []string{ + "extra error(s) found", + "spec.standard", + authoritativeMsg, + }, + }, + { + name: "Mismatch: Missing DV (Beta) + Enforced -> Disable Beta Gate", + imperativeErrors: field.ErrorList{errHVBeta}, + declarativeErrors: field.ErrorList{}, + enforced: true, + expectMismatches: true, + expectDetailsContaining: []string{ + "unmatched error(s) found", + "spec.beta", + disableBetaMsg, + }, + }, + { + name: "Mismatch: Extra DV (Alpha) + Enforced -> HV Authoritative (Override)", + imperativeErrors: field.ErrorList{}, + declarativeErrors: field.ErrorList{errDVAlpha}, + enforced: true, + expectMismatches: true, + expectDetailsContaining: []string{ + "extra error(s) found", + "spec.alpha", + authoritativeMsg, + }, + }, + { + name: "Fuzzy matching (different detail) - no mismatch", + imperativeErrors: field.ErrorList{errHVStandard}, + declarativeErrors: field.ErrorList{errDVStandardDiffDetail}, + expectMismatches: false, + }, + { + name: "Field normalization - no mismatch", + imperativeErrors: field.ErrorList{errHVStandard}, + declarativeErrors: field.ErrorList{errDVStandardDiffPath}, + normalizedRules: []field.NormalizationRule{ + { + Regexp: regexp.MustCompile(`spec.standardAliased`), + Replacement: "spec.standard", + }, + }, + expectMismatches: false, + }, + { + name: "Multiple mismatches - combined log info", + imperativeErrors: field.ErrorList{errHVBeta}, + declarativeErrors: field.ErrorList{errDVAlpha}, + enforced: true, + expectMismatches: true, + expectDetailsContaining: []string{ + "unmatched error(s) found", + "spec.beta", + disableBetaMsg, + "extra error(s) found", + "spec.alpha", + authoritativeMsg, + }, + }, + { + name: "Uncovered HV error - no mismatch (ignored)", + imperativeErrors: field.ErrorList{ + field.Invalid(pathOther, "val", "other").WithOrigin("min"), + }, + declarativeErrors: field.ErrorList{}, + expectMismatches: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + details := gatherDeclarativeValidationMismatches(tc.imperativeErrors, tc.declarativeErrors, tc.enforced, tc.normalizedRules) + // Check if mismatches were found if expected + if tc.expectMismatches && len(details) == 0 { + t.Errorf("Expected mismatches but got none") + } + // Check if details contain expected text + detailsStr := strings.Join(details, " ") + for _, expectedContent := range tc.expectDetailsContaining { + if !strings.Contains(detailsStr, expectedContent) { + t.Errorf("Expected details to contain: %q, but they didn't.\nDetails were:\n%s", + expectedContent, strings.Join(details, "\n")) + } + } + // If we don't expect any details, make sure none provided + if len(tc.expectDetailsContaining) == 0 && len(details) > 0 { + t.Errorf("Expected no details, but got %d details: %v", len(details), details) + } + }) + } +} + +// TestCompareDeclarativeErrorsAndEmitMismatches tests expected +// logging of mismatch information given match & mismatch error conditions. +func TestCompareDeclarativeErrorsAndEmitMismatches(t *testing.T) { + replicasPath := field.NewPath("spec").Child("replicas") + minReadySecondsPath := field.NewPath("spec").Child("minReadySeconds") + + errA := field.Invalid(replicasPath, nil, "regular error A") + errB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") + coveredErrB := field.Invalid(minReadySecondsPath, -1, "covered error B").WithOrigin("minimum") + coveredErrB.CoveredByDeclarative = true + + testCases := []struct { + name string + imperativeErrs field.ErrorList + declarativeErrs field.ErrorList + enforced bool + expectLogs bool + expectedRegex string + }{ + { + name: "mismatched errors, log info", + imperativeErrs: field.ErrorList{coveredErrB}, + declarativeErrs: field.ErrorList{errA}, + enforced: true, + expectLogs: true, + // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] + expectedRegex: "E.*Unexpected difference between hand written validation and declarative validation error results.*Consider disabling the DeclarativeValidationBeta feature gate to keep data persisted in etcd consistent with prior versions of Kubernetes", + }, + { + name: "matching errors, don't log info", + imperativeErrs: field.ErrorList{coveredErrB}, + declarativeErrs: field.ErrorList{errB}, + enforced: true, + expectLogs: false, + expectedRegex: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + klog.SetOutput(&buf) + klog.LogToStderr(false) + defer klog.LogToStderr(true) + ctx := context.Background() + + compareDeclarativeErrorsAndEmitMismatches(ctx, tc.imperativeErrs, tc.declarativeErrs, tc.enforced, "test_validationIdentifier", nil) + + klog.Flush() + logOutput := buf.String() + + if tc.expectLogs { + matched, err := regexp.MatchString(tc.expectedRegex, logOutput) + if err != nil { + t.Fatalf("Bad regex: %v", err) + } + if !matched { + t.Errorf("Expected log output to match %q, but got:\n%s", tc.expectedRegex, logOutput) + } + } else if len(logOutput) > 0 { + t.Errorf("Expected no mismatch logs, but found: %s", logOutput) + } + }) + } +} + +func TestWithRecover(t *testing.T) { + ctx := context.Background() + scheme := runtime.NewScheme() + var options []string + obj := &runtime.Unknown{} + + testCases := []struct { + name string + validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList + enforcementEnabled bool + wantErrs field.ErrorList + expectLogRegex string + }{ + { + name: "no panic", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + return field.ErrorList{ + field.Invalid(field.NewPath("field"), "value", "reason"), + } + }, + enforcementEnabled: false, + wantErrs: field.ErrorList{ + field.Invalid(field.NewPath("field"), "value", "reason"), + }, + expectLogRegex: "", + }, + { + name: "panic with enforcement disabled", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + panic("test panic") + }, + enforcementEnabled: false, + wantErrs: nil, + // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] + expectLogRegex: "E.*panic during declarative validation: test panic", + }, + { + name: "panic with enforcement enabled", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + panic("test panic") + }, + enforcementEnabled: true, + wantErrs: field.ErrorList{ + field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic")), + }, + expectLogRegex: "", + }, + { + name: "nil return, no panic", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + return nil + }, + enforcementEnabled: false, + wantErrs: nil, + expectLogRegex: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + klog.SetOutput(&buf) + klog.LogToStderr(false) + defer klog.LogToStderr(true) + + // Pass the enforcement flag to panicSafeValidateFunc + wrapped := panicSafeValidateFunc(tc.validateFn, tc.enforcementEnabled, "test_validationIdentifier") + gotErrs := wrapped(ctx, scheme, obj, nil, &validationConfigOption{opType: operation.Create, options: options}) + + klog.Flush() + logOutput := buf.String() + + // Compare gotErrs vs. tc.wantErrs + if !equalErrorLists(gotErrs, tc.wantErrs) { + t.Errorf("panicSafeValidateFunc() gotErrs = %#v, want %#v", gotErrs, tc.wantErrs) + } + + // Check logs if needed + if tc.expectLogRegex != "" { + matched, err := regexp.MatchString(tc.expectLogRegex, logOutput) + if err != nil { + t.Fatalf("Bad regex: %v", err) + } + if !matched { + t.Errorf("Expected log output %q, but got:\n%s", tc.expectLogRegex, logOutput) + } + } else if strings.Contains(logOutput, "panic during declarative validation") { + t.Errorf("Unexpected panic log found: %s", logOutput) + } + }) + } +} + +func TestWithRecoverUpdate(t *testing.T) { + ctx := context.Background() + scheme := runtime.NewScheme() + var options []string + obj := &runtime.Unknown{} + oldObj := &runtime.Unknown{} + + testCases := []struct { + name string + validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList + enforcementEnabled bool + wantErrs field.ErrorList + expectLogRegex string + }{ + { + name: "no panic", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + return field.ErrorList{ + field.Invalid(field.NewPath("field"), "value", "reason"), + } + }, + enforcementEnabled: false, + wantErrs: field.ErrorList{ + field.Invalid(field.NewPath("field"), "value", "reason"), + }, + expectLogRegex: "", + }, + { + name: "panic with enforcement disabled", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + panic("test update panic") + }, + enforcementEnabled: false, + wantErrs: nil, + // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] + expectLogRegex: "E.*panic during declarative validation: test update panic", + }, + { + name: "panic with enforcement enabled", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + panic("test update panic") + }, + enforcementEnabled: true, + wantErrs: field.ErrorList{ + field.InternalError(nil, fmt.Errorf("panic during declarative validation: test update panic")), + }, + expectLogRegex: "", + }, + { + name: "nil return, no panic", + validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *validationConfigOption) field.ErrorList { + return nil + }, + enforcementEnabled: false, + wantErrs: nil, + expectLogRegex: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + klog.SetOutput(&buf) + klog.LogToStderr(false) + defer klog.LogToStderr(true) + + // Pass the enforcement flag to panicSafeValidateUpdateFunc + wrapped := panicSafeValidateFunc(tc.validateFn, tc.enforcementEnabled, "test_validationIdentifier") + gotErrs := wrapped(ctx, scheme, obj, oldObj, &validationConfigOption{opType: operation.Update, options: options}) + + klog.Flush() + logOutput := buf.String() + + // Compare gotErrs with wantErrs + if !equalErrorLists(gotErrs, tc.wantErrs) { + t.Errorf("panicSafeValidateUpdateFunc() gotErrs = %#v, want %#v", gotErrs, tc.wantErrs) + } + + // Verify log output + if tc.expectLogRegex != "" { + matched, err := regexp.MatchString(tc.expectLogRegex, logOutput) + if err != nil { + t.Fatalf("Bad regex: %v", err) + } + if !matched { + t.Errorf("Expected log pattern %q, but got:\n%s", tc.expectLogRegex, logOutput) + } + } else if strings.Contains(logOutput, "panic during declarative validation") { + t.Errorf("Unexpected panic log found: %s", logOutput) + } + }) + } +} + +func TestRecordDuplicateValidationErrors(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + qualifiedKind schema.GroupKind + errs field.ErrorList + expectedMetric string + }{ + { + name: "detect duplicates and increment metric", + qualifiedKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, + errs: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("replicas"), -1, "must be greater than or equal to 0").WithOrigin("minimum"), + field.Invalid(field.NewPath("spec").Child("replicas"), -1, "must be greater than or equal to 0").WithOrigin("minimum"), + field.Invalid(field.NewPath("spec").Child("selector"), &metav1.LabelSelector{MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}}, "empty selector is invalid for deployment"), + field.Invalid(field.NewPath("spec").Child("selector"), &metav1.LabelSelector{MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}}, "empty selector is invalid for deployment"), + }, + expectedMetric: ` + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 2 + `, + }, + { + name: "detect duplicates with all fields but origin being equal", + qualifiedKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, + errs: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("replicas"), -1, "must be greater than or equal to 0").WithOrigin("minimum"), + field.Invalid(field.NewPath("spec").Child("replicas"), -1, "must be greater than or equal to 0").WithOrigin("min"), + }, + expectedMetric: ` + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 1 + `, + }, + { + name: "no duplicates", + qualifiedKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"}, + errs: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("replicas"), -1, "must be greater than or equal to 0").WithOrigin("minimum"), + field.Invalid(field.NewPath("spec").Child("selector"), &metav1.LabelSelector{MatchLabels: map[string]string{}, MatchExpressions: []metav1.LabelSelectorRequirement{}}, "empty selector is invalid for deployment"), + }, + expectedMetric: ` + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 0 + `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer legacyregistry.Reset() + defer validation.ResetValidationMetricsInstance() + RecordDuplicateValidationErrors(ctx, tc.qualifiedKind, tc.errs) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.expectedMetric), "apiserver_validation_duplicate_validation_error_total"); err != nil { + t.Fatal(err) + } + }) + } +} + +func equalErrorLists(a, b field.ErrorList) bool { + // If both are nil, consider them equal + if a == nil && b == nil { + return true + } + // If one is nil and the other not, they're different + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + // Both non-nil: do a normal DeepEqual + return reflect.DeepEqual(a, b) +} + +func TestMetricIdentifier(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(schema.GroupVersion{Version: "v1"}, &v1.Pod{}) + + testCases := []struct { + name string + opType operation.Type + obj runtime.Object + scheme *runtime.Scheme + subresource string + expected string + expectErr bool + }{ + { + name: "with subresource", + opType: operation.Create, + obj: &v1.Pod{TypeMeta: metav1.TypeMeta{Kind: "Pod"}}, + scheme: scheme, + subresource: "status", + expected: "pod_status_create", + expectErr: false, + }, + { + name: "without subresource", + opType: operation.Update, + obj: &v1.Pod{TypeMeta: metav1.TypeMeta{Kind: "Pod"}}, + scheme: scheme, + expected: "pod_update", + expectErr: false, + }, + { + name: "unknown operation", + opType: 3, // not a valid operation.Type + obj: &v1.Pod{TypeMeta: metav1.TypeMeta{Kind: "Pod"}}, + scheme: scheme, + expected: "pod_unknown_op", + expectErr: true, + }, + { + name: "no request info and no kind", + opType: operation.Create, + obj: nil, + expected: "unknown_resource_create", + expectErr: true, + }, + { + name: "known type without kind", + opType: operation.Update, + obj: &v1.Pod{}, + scheme: scheme, + expected: "pod_update", + expectErr: false, + }, + { + name: "unknown type with scheme", + opType: operation.Create, + obj: &runtime.Unknown{}, // Not registered in the scheme + scheme: scheme, + expected: "unknown_resource_create", + expectErr: true, + }, + { + name: "unknown type without scheme", + opType: operation.Type(4), + obj: &runtime.Unknown{}, // Not registered in the scheme + expected: "unknown_resource_unknown_op", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + if tc.obj != nil { + ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{ + Subresource: tc.subresource, + }) + } + + result, err := metricIdentifier(ctx, tc.scheme, tc.obj, tc.opType) + if (err != nil) != tc.expectErr { + t.Errorf("expected error: %v, got: %v", tc.expectErr, err) + } + if result != tc.expected { + t.Errorf("expected: %s, got: %s", tc.expected, result) + } + }) + } +} + +func TestValidateDeclarativelyWithMigrationChecks(t *testing.T) { + // Standard Lifecycle (Enforced by default in explicit strategy) + // Standard HV error marked as covered. In a fully migrated state, this should be deleted from source. + // We include it here to verify it persists (duplicate) if not deleted, rather than being implicitly filtered. + errHVStandardCovered := field.Forbidden(field.NewPath("spec", "standard"), "imperative standard").MarkCoveredByDeclarative() + errDVStandard := field.Forbidden(field.NewPath("spec", "standard"), "decStandard") + + // Additional Declarative (No HV counterpart) + errDVAdditional := field.Invalid(field.NewPath("spec", "additional"), "decAdditional", "declarative additional") + + // Beta Lifecycle (Gated by DeclarativeValidationBeta) + errHVBetaCovered := field.Forbidden(field.NewPath("spec", "beta"), "imperative beta").MarkCoveredByDeclarative().MarkBeta() + errDVBeta := field.Invalid(field.NewPath("spec", "beta"), "decBeta", "declarative beta").MarkBeta() + + // Alpha Lifecycle (Shadowed) + // Alpha rules should NOT mark HV as covered, so HV remains authoritative. + errHVAlpha := field.Forbidden(field.NewPath("spec", "alpha"), "imperative alpha") + errDVAlpha := field.Invalid(field.NewPath("spec", "alpha"), "decAlpha", "declarative alpha").MarkAlpha() + + testCases := []struct { + name string + dvFeatureEnabled bool + declarativeEnforcement bool + betaGateEnabled bool + imperativeErrors field.ErrorList + declarativeErrors field.ErrorList + expectedErrors field.ErrorList + shouldPanic bool + }{ + { + name: "Feature Disabled, Not Enforced -> Skips declarative, Returns HV", + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard}, + expectedErrors: field.ErrorList{errHVStandardCovered}, + }, + { + name: "Feature Disabled, Enforced -> Enforces Standard (HV kept+DV returned, duplicate expected if HV not deleted)", + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard, errDVAdditional}, + expectedErrors: field.ErrorList{errHVStandardCovered, errDVStandard, errDVAdditional}, + }, + { + name: "Feature Enabled, Not Enforced -> Returns imperative (Shadow Mode)", + dvFeatureEnabled: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard}, + expectedErrors: field.ErrorList{errHVStandardCovered}, + }, + { + name: "Feature Enabled, Enforced -> Enforces Standard (HV kept+DV returned, duplicate expected if HV not deleted)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{errDVStandard, errDVAdditional}, + expectedErrors: field.ErrorList{errHVStandardCovered, errDVStandard, errDVAdditional}, + }, + { + name: "Feature Disabled, Enforced, Panics -> Returns InternalError", + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + shouldPanic: true, + // Standard HV is kept. Panic error appended. + expectedErrors: append(field.ErrorList{errHVStandardCovered}, field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic"))), + }, + { + name: "Feature Enabled, Enforced, InternalError -> Returns InternalError", + dvFeatureEnabled: true, + declarativeEnforcement: true, + imperativeErrors: field.ErrorList{errHVStandardCovered}, + declarativeErrors: field.ErrorList{field.InternalError(nil, fmt.Errorf("internal error"))}, + // Standard HV kept. Internal error appended. + expectedErrors: field.ErrorList{errHVStandardCovered, field.InternalError(nil, fmt.Errorf("internal error"))}, + }, + { + name: "Enforced, Beta Gate Enabled -> Enforces Beta (HV removed, DV returned)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: true, + imperativeErrors: field.ErrorList{errHVBetaCovered}, + declarativeErrors: field.ErrorList{errDVBeta}, + expectedErrors: field.ErrorList{errDVBeta}, + }, + { + name: "Enforced, Beta Gate Disabled -> Shadows Beta (HV kept, DV hidden)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: false, + imperativeErrors: field.ErrorList{errHVBetaCovered}, + declarativeErrors: field.ErrorList{errDVBeta}, + expectedErrors: field.ErrorList{errHVBetaCovered}, + }, + { + name: "Enforced, Alpha -> Shadows Alpha (HV kept, DV hidden)", + dvFeatureEnabled: true, + declarativeEnforcement: true, + betaGateEnabled: true, + imperativeErrors: field.ErrorList{errHVAlpha}, + declarativeErrors: field.ErrorList{errDVAlpha}, + expectedErrors: field.ErrorList{errHVAlpha}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Set feature gate + if !tc.dvFeatureEnabled { + featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.35")) + } else { + // Only set Beta gate if we are not emulating an older version where it doesn't exist + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationBeta, tc.betaGateEnabled) + } + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, tc.dvFeatureEnabled) + + // Setup scheme for this run + localScheme := runtime.NewScheme() + localScheme.AddKnownTypes(schema.GroupVersion{Group: "", Version: "v1"}, &v1.Pod{}) + localScheme.AddValidationFunc(&v1.Pod{}, func(ctx context.Context, op operation.Operation, object, oldObject interface{}) field.ErrorList { + if tc.shouldPanic { + panic("test panic") + } + return tc.declarativeErrors + }) + + // Setup context + ctx := genericapirequest.WithRequestInfo(context.Background(), &genericapirequest.RequestInfo{ + APIGroup: "", + APIVersion: "v1", + Resource: "pods", + }) + + obj := &v1.Pod{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, + ObjectMeta: metav1.ObjectMeta{Name: "test-pod"}, + } + + // Copy imperative errors because they might be modified/appended to + inputErrs := make(field.ErrorList, len(tc.imperativeErrors)) + copy(inputErrs, tc.imperativeErrors) + + opts := []ValidationConfig{} + if tc.declarativeEnforcement { + opts = append(opts, WithDeclarativeEnforcement()) + } + + gotErrs := ValidateDeclarativelyWithMigrationChecks(ctx, localScheme, obj, nil, inputErrs, operation.Create, opts...) + + if !equalErrorLists(gotErrs, tc.expectedErrors) { + t.Errorf("Expected errors: %v, got: %v", tc.expectedErrors, gotErrs) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/config.go index 3e8a6b8c0..f197b55cd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config.go @@ -19,6 +19,7 @@ package server import ( "context" "crypto/sha256" + "crypto/tls" "encoding/base32" "fmt" "net" @@ -55,6 +56,7 @@ import ( discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" "k8s.io/apiserver/pkg/endpoints/filterlatency" genericapifilters "k8s.io/apiserver/pkg/endpoints/filters" + "k8s.io/apiserver/pkg/endpoints/filters/impersonation" apiopenapi "k8s.io/apiserver/pkg/endpoints/openapi" apirequest "k8s.io/apiserver/pkg/endpoints/request" genericfeatures "k8s.io/apiserver/pkg/features" @@ -62,6 +64,7 @@ import ( "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/egressselector" genericfilters "k8s.io/apiserver/pkg/server/filters" + "k8s.io/apiserver/pkg/server/flagz" "k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/routes" "k8s.io/apiserver/pkg/server/routine" @@ -79,7 +82,6 @@ import ( "k8s.io/component-base/metrics/features" "k8s.io/component-base/metrics/prometheus/slis" "k8s.io/component-base/tracing" - "k8s.io/component-base/zpages/flagz" "k8s.io/klog/v2" openapicommon "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" @@ -364,6 +366,13 @@ type SecureServingInfo struct { // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). CipherSuites []uint16 + // CurvePreferences optionally specifies the set of allowed key exchange mechanisms for the server. + // The order of the list is ignored, and key exchange mechanisms + // are chosen by Go from this list using an internal preference order. + // If empty, the default Go curves will be used. + // Values are from the Go crypto/tls CurveID constants (https://golang.org/pkg/crypto/tls/#CurveID). + CurvePreferences []tls.CurveID + // HTTP2MaxStreamsPerConnection is the limit that the api server imposes on each client. // A value of zero means to use the default provided by golang's HTTP/2 support. HTTP2MaxStreamsPerConnection int @@ -572,6 +581,18 @@ func (c *Config) AddHealthChecks(healthChecks ...healthz.HealthChecker) { c.ReadyzChecks = append(c.ReadyzChecks, healthChecks...) } +// AddHealthzChecks adds the provided health checks to our config to be exposed by the +// healthz endpoint of our configured apiserver. +func (c *Config) AddHealthzChecks(healthChecks ...healthz.HealthChecker) { + c.HealthzChecks = append(c.HealthzChecks, healthChecks...) +} + +// AddLivezChecks adds the provided health checks to our config to be exposed by the +// livez endpoint of our configured apiserver. +func (c *Config) AddLivezChecks(healthChecks ...healthz.HealthChecker) { + c.LivezChecks = append(c.LivezChecks, healthChecks...) +} + // AddReadyzChecks adds a health check to our config to be exposed by the readyz endpoint // of our configured apiserver. func (c *Config) AddReadyzChecks(healthChecks ...healthz.HealthChecker) { @@ -817,6 +838,7 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G UnprotectedDebugSocket: debugSocket, listedPathProvider: apiServerHandler, + Flagz: c.Flagz, minRequestTimeout: time.Duration(c.MinRequestTimeout) * time.Second, ShutdownTimeout: c.RequestTimeout, @@ -1030,8 +1052,13 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler { } handler = filterlatency.TrackCompleted(handler) - handler = genericapifilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer) - handler = filterlatency.TrackStarted(handler, c.TracerProvider, "impersonation") + if c.FeatureGate.Enabled(genericfeatures.ConstrainedImpersonation) { + handler = impersonation.WithConstrainedImpersonation(handler, c.Authorization.Authorizer, c.Serializer) + handler = filterlatency.TrackStarted(handler, c.TracerProvider, "constrainedimpersonation") + } else { + handler = impersonation.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer) + handler = filterlatency.TrackStarted(handler, c.TracerProvider, "impersonation") + } handler = filterlatency.TrackCompleted(handler) handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyRuleEvaluator, c.LongRunningFunc) @@ -1123,7 +1150,7 @@ func installAPI(name string, s *GenericAPIServer, c *Config) { routes.Version{Version: c.EffectiveVersion.Info()}.Install(s.Handler.GoRestfulContainer) if c.EnableDiscovery { - wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(s.DiscoveryGroupManager, s.AggregatedDiscoveryGroupManager) + wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(s.DiscoveryGroupManager, s.AggregatedDiscoveryGroupManager, s.PeerAggregatedDiscoveryManager) s.Handler.GoRestfulContainer.Add(wrapped.GenerateWebService("/apis", metav1.APIGroupList{})) } if c.FlowControl != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_selfclient.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_selfclient.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_selfclient.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_selfclient.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_selfclient_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_selfclient_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_selfclient_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_selfclient_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_test.go index 577b99fc2..f497a9b25 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/config_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/config_test.go @@ -223,6 +223,7 @@ func TestNewWithDelegate(t *testing.T) { "/readyz/poststarthook/storage-object-count-tracker-hook", "/readyz/poststarthook/wrapping-post-start-hook", "/readyz/shutdown", + "/statusz", } checkExpectedPathsAtRoot(server.URL, expectedPaths, t) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/deleted_kinds.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/deleted_kinds.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/deleted_kinds.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/deleted_kinds.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/deleted_kinds_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/deleted_kinds_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/deleted_kinds_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/deleted_kinds_test.go index 20f38a98c..c6a5accdd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/deleted_kinds_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/deleted_kinds_test.go @@ -23,12 +23,12 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/dump" "k8s.io/apimachinery/pkg/util/sets" apimachineryversion "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/server/resourceconfig" serverstorage "k8s.io/apiserver/pkg/server/storage" + "k8s.io/utils/dump" "github.com/stretchr/testify/require" ) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/deprecated_insecure_serving.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/deprecated_insecure_serving.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/deprecated_insecure_serving.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/deprecated_insecure_serving.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/cert_key.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/cert_key.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/cert_key.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/cert_key.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/cert_key_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/cert_key_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/cert_key_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/cert_key_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/client_ca.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/client_ca.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/client_ca.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/client_ca.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/client_ca_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/client_ca_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/client_ca_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/client_ca_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/configmap_cafile_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/configmap_cafile_content.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/configmap_cafile_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/configmap_cafile_content.go index 845a45fab..5af3dbe86 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/configmap_cafile_content.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/configmap_cafile_content.go @@ -199,7 +199,7 @@ func (c *ConfigMapCAController) RunOnce(ctx context.Context) error { // Run starts the kube-apiserver and blocks until stopCh is closed. func (c *ConfigMapCAController) Run(ctx context.Context, workers int) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer c.queue.ShutDown() klog.InfoS("Starting controller", "name", c.name) @@ -209,7 +209,7 @@ func (c *ConfigMapCAController) Run(ctx context.Context, workers int) { go c.configMapInformer.Run(ctx.Done()) // wait for your secondary caches to fill before starting your work - if !cache.WaitForNamedCacheSync(c.name, ctx.Done(), c.preRunCaches...) { + if !cache.WaitForNamedCacheSyncWithContext(ctx, c.preRunCaches...) { return } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go index 0fcf82bd0..2f2195134 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_cafile_content.go @@ -155,7 +155,7 @@ func (c *DynamicFileCAContent) RunOnce(ctx context.Context) error { // Run starts the controller and blocks until stopCh is closed. func (c *DynamicFileCAContent) Run(ctx context.Context, workers int) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer c.queue.ShutDown() klog.InfoS("Starting controller", "name", c.name) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_serving_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_serving_content.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_serving_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_serving_content.go index e0dd0474b..268fe70b0 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_serving_content.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_serving_content.go @@ -129,7 +129,7 @@ func (c *DynamicCertKeyPairContent) RunOnce(ctx context.Context) error { // Run starts the controller and blocks until context is killed. func (c *DynamicCertKeyPairContent) Run(ctx context.Context, workers int) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer c.queue.ShutDown() klog.InfoS("Starting controller", "name", c.name) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_sni_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_sni_content.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/dynamic_sni_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/dynamic_sni_content.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/interfaces.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/interfaces.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/named_certificates.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/named_certificates.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/named_certificates.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/named_certificates.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/named_certificates_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/named_certificates_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/named_certificates_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/named_certificates_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/server_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/server_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/server_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/server_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/static_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/static_content.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/static_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/static_content.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/tlsconfig.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/tlsconfig.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/tlsconfig.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/tlsconfig.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/tlsconfig_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/tlsconfig_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/tlsconfig_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/tlsconfig_test.go index 322cd7141..0fe37628f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/tlsconfig_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/tlsconfig_test.go @@ -20,7 +20,7 @@ import ( "reflect" "testing" - "k8s.io/apimachinery/pkg/util/dump" + "k8s.io/utils/dump" ) var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY----- diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/union_content.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/union_content.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/union_content.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/union_content.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/dynamiccertificates/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/dynamiccertificates/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/config_test.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/config_test.go index 3eb72683f..441bc3224 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/config_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/config_test.go @@ -302,6 +302,52 @@ spec: expectedResult: nil, expectedError: ptr.To("invalid service configuration object \"DaemonSet\""), }, + { + name: "v1beta1 with tlsServerName", + createFile: true, + contents: ` +apiVersion: apiserver.k8s.io/v1beta1 +kind: EgressSelectorConfiguration +egressSelections: +- name: "cluster" + connection: + proxyProtocol: "HTTPConnect" + transport: + tcp: + url: "https://proxy.kube-system.svc.cluster.local:8443" + tlsConfig: + caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt" + clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key" + clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt" + tlsServerName: "konnectivity-server.example.com" +`, + expectedResult: &apiserver.EgressSelectorConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "", + APIVersion: "", + }, + EgressSelections: []apiserver.EgressSelection{ + { + Name: "cluster", + Connection: apiserver.Connection{ + ProxyProtocol: "HTTPConnect", + Transport: &apiserver.Transport{ + TCP: &apiserver.TCPTransport{ + URL: "https://proxy.kube-system.svc.cluster.local:8443", + TLSConfig: &apiserver.TLSConfig{ + CABundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt", + ClientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key", + ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt", + TLSServerName: "konnectivity-server.example.com", + }, + }, + }, + }, + }, + }, + }, + expectedError: nil, + }, } for _, tc := range testcases { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector.go index a38ef6464..b898ff327 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector.go @@ -22,6 +22,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io" "net" "net/http" "net/url" @@ -110,7 +111,14 @@ func lookupServiceName(name string) (EgressType, error) { func tunnelHTTPConnect(proxyConn net.Conn, proxyAddress, addr string) (net.Conn, error) { fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, "127.0.0.1") - br := bufio.NewReader(proxyConn) + + // As described in https://go.dev/issue/74633 a misbehaving proxy server + // can cause memory exhaustion in the client. The fix in https://go.dev/cl/698915 + // only covers http.Transport users. Apply the same limit here. + // + // Limit the size of the response headers the proxy server can send us. + br := bufio.NewReader(io.LimitReader(proxyConn, http.DefaultMaxHeaderBytes)) + res, err := http.ReadResponse(br, nil) if err != nil { proxyConn.Close() @@ -292,6 +300,7 @@ func getTLSConfig(t *apiserver.TLSConfig) (*tls.Config, error) { return &tls.Config{ Certificates: []tls.Certificate{clientCerts}, RootCAs: certPool, + ServerName: t.TLSServerName, }, nil } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector_test.go similarity index 88% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector_test.go index 896b24461..875cdacd8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/egress_selector_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/egress_selector_test.go @@ -21,6 +21,8 @@ import ( "errors" "fmt" "net" + "os" + "path/filepath" "strings" "testing" "time" @@ -29,6 +31,7 @@ import ( utilnet "k8s.io/apimachinery/pkg/util/net" "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/server/egressselector/metrics" + certutil "k8s.io/client-go/util/cert" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" testingclock "k8s.io/utils/clock/testing" @@ -343,3 +346,61 @@ konnectivity_network_proxy_client_client_connections{status="dialing"} 1 }) } } + +func TestGetTLSConfig(t *testing.T) { + tempDir := t.TempDir() + + certPEM, keyPEM, err := certutil.GenerateSelfSignedCertKey("localhost", nil, nil) + if err != nil { + t.Fatalf("Failed to generate test certificates: %v", err) + } + + certPath := filepath.Join(tempDir, "cert.crt") + keyPath := filepath.Join(tempDir, "cert.key") + if err := os.WriteFile(certPath, certPEM, 0600); err != nil { + t.Fatalf("Failed to write cert file: %v", err) + } + if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil { + t.Fatalf("Failed to write key file: %v", err) + } + + testcases := []struct { + name string + tlsConfig *apiserver.TLSConfig + expectedServerName string + }{ + { + name: "with TLSServerName set", + tlsConfig: &apiserver.TLSConfig{ + CABundle: certPath, + ClientCert: certPath, + ClientKey: keyPath, + TLSServerName: "custom-server.example.com", + }, + expectedServerName: "custom-server.example.com", + }, + { + name: "without TLSServerName (empty)", + tlsConfig: &apiserver.TLSConfig{ + CABundle: certPath, + ClientCert: certPath, + ClientKey: keyPath, + TLSServerName: "", + }, + expectedServerName: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tlsConfig, err := getTLSConfig(tc.tlsConfig) + if err != nil { + t.Fatalf("getTLSConfig returned unexpected error: %v", err) + } + + if tlsConfig.ServerName != tc.expectedServerName { + t.Errorf("expected ServerName %q, got %q", tc.expectedServerName, tlsConfig.ServerName) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/egressselector/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/egressselector/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/content_type.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/content_type.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/content_type.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/content_type.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/content_type_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/content_type_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/content_type_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/content_type_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/cors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/cors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/cors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/cors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/cors_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/cors_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/cors_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/cors_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/goaway.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/goaway.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/goaway.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/goaway.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/goaway_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/goaway_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/goaway_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/goaway_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/hsts.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/hsts.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/hsts.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/hsts.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/longrunning.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/longrunning.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/longrunning.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/longrunning.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/maxinflight.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/maxinflight.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/maxinflight.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/maxinflight.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/maxinflight_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/maxinflight_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/maxinflight_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/maxinflight_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/priority-and-fairness.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/priority-and-fairness.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/priority-and-fairness.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/priority-and-fairness.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/priority-and-fairness_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/priority-and-fairness_test.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/priority-and-fairness_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/priority-and-fairness_test.go index 18d99292c..c218a57e1 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/priority-and-fairness_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/priority-and-fairness_test.go @@ -712,7 +712,7 @@ func TestPriorityAndFairnessWithPanicRecoveryAndTimeoutFilter(t *testing.T) { firstRequestPathPanic, secondRequestPathShouldWork := "/request/panic-as-designed", "/request/should-succeed-as-expected" firstHandlerDoneCh, secondHandlerDoneCh := make(chan struct{}), make(chan struct{}) requestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headerMatcher.inspect(t, w, fsName, plName) + headerMatcher.inspect(t, w, r.Context(), fsName, plName) switch { case r.URL.Path == firstRequestPathPanic: close(firstHandlerDoneCh) @@ -785,7 +785,7 @@ func TestPriorityAndFairnessWithPanicRecoveryAndTimeoutFilter(t *testing.T) { rquestTimesOutPath := "/request/time-out-as-designed" reqHandlerCompletedCh, callerRoundTripDoneCh := make(chan struct{}), make(chan struct{}) requestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headerMatcher.inspect(t, w, fsName, plName) + headerMatcher.inspect(t, w, r.Context(), fsName, plName) if r.URL.Path == rquestTimesOutPath { defer close(reqHandlerCompletedCh) @@ -858,7 +858,7 @@ func TestPriorityAndFairnessWithPanicRecoveryAndTimeoutFilter(t *testing.T) { reqHandlerErrCh, callerRoundTripDoneCh := make(chan error, 1), make(chan struct{}) rquestTimesOutPath := "/request/time-out-as-designed" requestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headerMatcher.inspect(t, w, fsName, plName) + headerMatcher.inspect(t, w, r.Context(), fsName, plName) if r.URL.Path == rquestTimesOutPath { <-callerRoundTripDoneCh @@ -937,7 +937,7 @@ func TestPriorityAndFairnessWithPanicRecoveryAndTimeoutFilter(t *testing.T) { rquestTimesOutPath := "/request/time-out-as-designed" reqHandlerErrCh, callerRoundTripDoneCh := make(chan error, 1), make(chan struct{}) requestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headerMatcher.inspect(t, w, fsName, plName) + headerMatcher.inspect(t, w, r.Context(), fsName, plName) if r.URL.Path == rquestTimesOutPath { @@ -1016,7 +1016,7 @@ func TestPriorityAndFairnessWithPanicRecoveryAndTimeoutFilter(t *testing.T) { firstReqHandlerErrCh, firstReqInProgressCh := make(chan error, 1), make(chan struct{}) firstReqRoundTripDoneCh, secondReqRoundTripDoneCh := make(chan struct{}), make(chan struct{}) requestHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - headerMatcher.inspect(t, w, fsName, plName) + headerMatcher.inspect(t, w, r.Context(), fsName, plName) switch { case r.URL.Path == firstRequestTimesOutPath: close(firstReqInProgressCh) @@ -1188,28 +1188,42 @@ func newHTTP2ServerWithClient(handler http.Handler, clientTimeout time.Duration) type headerMatcher struct{} // verifies that the expected flow schema and priority level UIDs are attached to the header. -func (m *headerMatcher) inspect(t *testing.T, w http.ResponseWriter, expectedFS, expectedPL string) { +func (m *headerMatcher) inspect(t *testing.T, w http.ResponseWriter, ctx context.Context, expectedFS, expectedPL string) { t.Helper() - err := func() error { - if w == nil { - return fmt.Errorf("expected a non nil HTTP response") - } - key := flowcontrol.ResponseHeaderMatchedFlowSchemaUID - if value := w.Header().Get(key); expectedFS != value { - return fmt.Errorf("expected HTTP header %s to have value %q, but got: %q", key, expectedFS, value) + if w == nil { + t.Errorf("expected a non nil HTTP response") + return + } + + checkHeader := func(key, expected string) { + actual := w.Header().Get(key) + if actual == expected { + return } - key = flowcontrol.ResponseHeaderMatchedPriorityLevelConfigurationUID - if value := w.Header().Get(key); expectedPL != value { - return fmt.Errorf("expected HTTP header %s to have value %q, but got %q", key, expectedPL, value) + // Header writes are best-effort once the request context is canceled. + if actual == "" { + select { + case <-ctx.Done(): + t.Logf("Skipping APF header assertion for %s: request context already done", key) + return + default: + } + + // The timeout filter may have already committed the response + // before APF had a chance to attach its headers. + if ctx.Err() != nil { + t.Logf("Skipping APF header assertion for %s: response committed after timeout", key) + return + } } - return nil - }() - if err == nil { - return + + t.Errorf("expected HTTP header %s to have value %q, but got: %q", key, expected, actual) } - t.Errorf("Expected APF headers to match, but got: %v", err) + + checkHeader(flowcontrol.ResponseHeaderMatchedFlowSchemaUID, expectedFS) + checkHeader(flowcontrol.ResponseHeaderMatchedPriorityLevelConfigurationUID, expectedPL) } // when a request panics, http2 resets the stream with an INTERNAL_ERROR message diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/timeout.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/timeout.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/timeout.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/timeout.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/timeout_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/timeout_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/timeout_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/timeout_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/waitgroup.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/waitgroup.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/waitgroup.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/waitgroup.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/watch_termination.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/watch_termination.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/watch_termination.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/watch_termination.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/watch_termination_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/watch_termination_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/watch_termination_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/watch_termination_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/with_retry_after.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/with_retry_after.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/with_retry_after.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/with_retry_after.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/with_retry_after_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/with_retry_after_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/with_retry_after_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/with_retry_after_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/wrap.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/wrap.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/wrap.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/wrap.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/wrap_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/wrap_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/filters/wrap_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/filters/wrap_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/doc.go similarity index 68% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/doc.go index 703f467f9..42d5fcef5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/doc.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,9 +15,8 @@ limitations under the License. */ // +k8s:deepcopy-gen=package -// +k8s:conversion-gen=k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission -// +k8s:defaulter-gen=TypeMeta -// +groupName=apiserver.config.k8s.io +// +k8s:openapi-gen=true +// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.flagz.api.v1alpha1 -// Package v1alpha1 is the v1alpha1 version of the API. +// Package v1alpha1 contains API Schema definitions for the flagz v1alpha1 API group package v1alpha1 diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/register.go new file mode 100644 index 000000000..d2a1f08bc --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/register.go @@ -0,0 +1,47 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +const ( + GroupName = "config.k8s.io" + Version = "v1alpha1" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + +var ( + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that adds this group's types to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Flagz{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/types.go similarity index 54% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/types.go index a49a6a813..c0ce97369 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/types.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/types.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors. +Copyright 2025 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,14 +16,22 @@ limitations under the License. package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// WebhookAdmission provides configuration for the webhook admission controller. -type WebhookAdmission struct { +// Flagz is the structured response for the /flagz endpoint. +type Flagz struct { + // TypeMeta is the type metadata for the object. metav1.TypeMeta `json:",inline"` - - // KubeConfigFile is the path to the kubeconfig file. - KubeConfigFile string `json:"kubeConfigFile"` + // Standard object's metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Flags contains the command-line flags and their values. + // The keys are the flag names and the values are the flag values, + // possibly with confidential values redacted. + // +optional + Flags map[string]string `json:"flags,omitempty"` } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go similarity index 76% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go index f997f4aba..b32aabef6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.deepcopy.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.deepcopy.go @@ -26,24 +26,32 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WebhookAdmission) DeepCopyInto(out *WebhookAdmission) { +func (in *Flagz) DeepCopyInto(out *Flagz) { *out = *in out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Flags != nil { + in, out := &in.Flags, &out.Flags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookAdmission. -func (in *WebhookAdmission) DeepCopy() *WebhookAdmission { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagz. +func (in *Flagz) DeepCopy() *Flagz { if in == nil { return nil } - out := new(WebhookAdmission) + out := new(Flagz) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *WebhookAdmission) DeepCopyObject() runtime.Object { +func (in *Flagz) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.defaults.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go similarity index 64% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.defaults.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go index 5070cb91b..d953fff3c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1/zz_generated.defaults.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1alpha1/zz_generated.model_name.go @@ -17,17 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by defaulter-gen. DO NOT EDIT. +// Code generated by openapi-gen. DO NOT EDIT. package v1alpha1 -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Flagz) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.server.flagz.api.v1alpha1.Flagz" } diff --git a/third_party/k8s.io/apiserver-v0.34.1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/doc.go similarity index 66% rename from third_party/k8s.io/apiserver-v0.34.1/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/doc.go index 573d9e39b..bc8cffc0a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/doc.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,4 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package apiserver +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.flagz.api.v1beta1 + +// Package v1beta1 contains API Schema definitions for the flagz v1beta1 API group +package v1beta1 diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/register.go new file mode 100644 index 000000000..f66acc7dd --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/register.go @@ -0,0 +1,47 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +const ( + GroupName = "config.k8s.io" + Version = "v1beta1" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + +var ( + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that adds this group's types to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Flagz{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/types.go new file mode 100644 index 000000000..403436fdc --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/types.go @@ -0,0 +1,37 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// Flagz is the structured response for the /flagz endpoint. +type Flagz struct { + // TypeMeta is the type metadata for the object. + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Flags contains the command-line flags and their values. + // The keys are the flag names and the values are the flag values, + // possibly with confidential values redacted. + // +optional + Flags map[string]string `json:"flags,omitempty"` +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..1d5e52605 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,59 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Flagz) DeepCopyInto(out *Flagz) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Flags != nil { + in, out := &in.Flags, &out.Flags + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Flagz. +func (in *Flagz) DeepCopy() *Flagz { + if in == nil { + return nil + } + out := new(Flagz) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Flagz) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go new file mode 100644 index 000000000..d475a0aa6 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/api/v1beta1/zz_generated.model_name.go @@ -0,0 +1,27 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1beta1 + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Flagz) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.server.flagz.api.v1beta1.Flagz" +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader.go new file mode 100644 index 000000000..40fe09b45 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader.go @@ -0,0 +1,52 @@ +/* +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 flagz + +import ( + "github.com/spf13/pflag" + cliflag "k8s.io/component-base/cli/flag" +) + +type Reader interface { + GetFlagz() map[string]string +} + +// NamedFlagSetsReader implements Reader for cliflag.NamedFlagSets +type NamedFlagSetsReader struct { + FlagSets cliflag.NamedFlagSets +} + +func (n NamedFlagSetsReader) GetFlagz() map[string]string { + return convertNamedFlagSetToFlags(&n.FlagSets) +} + +func convertNamedFlagSetToFlags(flagSets *cliflag.NamedFlagSets) map[string]string { + flags := make(map[string]string) + for _, fs := range flagSets.FlagSets { + fs.VisitAll(func(flag *pflag.Flag) { + if flag.Value != nil { + value := flag.Value.String() + if set, ok := flag.Annotations["classified"]; ok && len(set) > 0 { + value = "CLASSIFIED" + } + flags[flag.Name] = value + } + }) + } + + return flags +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader_test.go new file mode 100644 index 000000000..3d634d602 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagreader_test.go @@ -0,0 +1,95 @@ +/* +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 flagz + +import ( + "reflect" + "testing" + + "github.com/spf13/pflag" + + "k8s.io/component-base/cli/flag" +) + +func TestConvertNamedFlagSetToFlags(t *testing.T) { + tests := []struct { + name string + flagSets *flag.NamedFlagSets + want map[string]string + }{ + { + name: "basic flags", + flagSets: &flag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": flagSet(t, map[string]flagValue{ + "flag1": {value: "value1", sensitive: false}, + "flag2": {value: "value2", sensitive: false}, + }), + }, + }, + want: map[string]string{ + "flag1": "value1", + "flag2": "value2", + }, + }, + { + name: "classified flags", + flagSets: &flag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": flagSet(t, map[string]flagValue{ + "secret1": {value: "value1", sensitive: true}, + "flag2": {value: "value2", sensitive: false}, + }), + }, + }, + want: map[string]string{ + "flag2": "value2", + "secret1": "CLASSIFIED", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertNamedFlagSetToFlags(tt.flagSets) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ConvertNamedFlagSetToFlags() = %v, want %v", got, tt.want) + } + }) + } +} + +type flagValue struct { + value string + sensitive bool +} + +func flagSet(t *testing.T, flags map[string]flagValue) *pflag.FlagSet { + fs := pflag.NewFlagSet("test-set", pflag.ContinueOnError) + for flagName, flagVal := range flags { + flagValue := "" + fs.StringVar(&flagValue, flagName, flagVal.value, "test-usage") + if flagVal.sensitive { + err := fs.SetAnnotation(flagName, "classified", []string{"true"}) + if err != nil { + t.Fatalf("unexpected error when setting flag annotation: %v", err) + } + } + } + + return fs +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz.go new file mode 100644 index 000000000..cb6df7b5d --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz.go @@ -0,0 +1,320 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flagz + +import ( + "fmt" + "net/http" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/endpoints/responsewriter" + "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" + "k8s.io/apiserver/pkg/server/flagz/api/v1beta1" + "k8s.io/apiserver/pkg/server/flagz/negotiate" + utilfeature "k8s.io/apiserver/pkg/util/feature" +) + +var ( + v1alpha1FlagzKind = v1alpha1.SchemeGroupVersion.WithKind("Flagz") + v1beta1FlagzKind = v1beta1.SchemeGroupVersion.WithKind("Flagz") + recognizedStructuredKinds = map[schema.GroupVersionKind]bool{ + v1alpha1FlagzKind: true, + v1beta1FlagzKind: true, + } +) + +const DefaultFlagzPath = "/flagz" + +// flagzCodecFactory wraps a CodecFactory to filter out unsupported media types (like protobuf) +// from the supported media types list, so error messages only show actually supported types. +type flagzCodecFactory struct { + serializer.CodecFactory + supportedMediaTypes []runtime.SerializerInfo +} + +type mux interface { + Handle(path string, handler http.Handler) +} + +// Install installs the flagz endpoint to the given mux. +func Install(m mux, componentName string, flagReader Reader, opts ...Option) { + reg := ®istry{ + reader: flagReader, + deprecatedVersionsMap: map[string]bool{"v1alpha1": true}, + } + for _, opt := range opts { + opt(reg) + } + + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) + filteredCodecFactory, err := newFlagzCodecFactory(scheme, componentName, reg.reader) + if err != nil { + utilruntime.HandleError(err) + } + restrictions := negotiate.FlagzEndpointRestrictions{ + RecognizedStructuredKinds: recognizedStructuredKinds, + } + m.Handle(DefaultFlagzPath, handleFlagz(componentName, reg, filteredCodecFactory, restrictions)) +} + +// newFlagzCodecFactory creates a codec factory with the standard serializers for flagz, +// filtering out unsupported media types (e.g., protobuf). +func newFlagzCodecFactory(scheme *runtime.Scheme, componentName string, flagReader Reader) (*flagzCodecFactory, error) { + codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ + serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { + textSerializer := flagzTextSerializer{componentName, flagReader} + return runtime.SerializerInfo{ + MediaType: "text/plain", + MediaTypeType: "text", + MediaTypeSubType: "plain", + EncodesAsText: true, + Serializer: textSerializer, + PrettySerializer: textSerializer, + } + }), + } + // TODO: remove this explicit check when https://github.com/kubernetes/enhancements/pull/5740 is implemented. + if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) { + codecFactoryOpts = append(codecFactoryOpts, serializer.WithSerializer(cbor.NewSerializerInfo)) + } + + codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) + allTypes := codecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + + var unknownTypes []string + for _, info := range allTypes { + switch info.MediaType { + // Supported media types + case "text/plain", runtime.ContentTypeJSON, runtime.ContentTypeYAML, runtime.ContentTypeCBOR: + filtered = append(filtered, info) + // Unsupported media types + case runtime.ContentTypeProtobuf: + continue + default: + unknownTypes = append(unknownTypes, info.MediaType) + } + } + + var err error + if len(unknownTypes) > 0 { + err = fmt.Errorf("flagz: unknown media type(s) %v, excluding from supported types", unknownTypes) + } + + return &flagzCodecFactory{ + CodecFactory: codecFactory, + supportedMediaTypes: filtered, + }, err +} + +func (f *flagzCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { + return f.supportedMediaTypes +} + +func handleFlagz(componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(r.Context()) + if !ok { + requestReceivedTimestamp = time.Now() + } + delegate := &metrics.ResponseWriterDelegator{ResponseWriter: w} + w = responsewriter.WrapForHTTP1Or2(delegate) + + // Use MonitorRequest instead of InstrumentHandlerFunc because the group, + // version, and deprecated status depend on per-request content negotiation. + // For text/plain requests, group and version remain empty. For structured + // responses (JSON/YAML/CBOR), they are set to the negotiated API group and + // version (e.g., config.k8s.io/v1alpha1). + var group, version string + var deprecated bool + defer func() { + metrics.MonitorRequest(r, "GET", group, version, + "flagz", // resource + "", // subresource + "", // scope + componentName, // component + deprecated, + "", // removedRelease + delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp)) + }() + + acceptHeader := r.Header.Get("Accept") + if strings.TrimSpace(acceptHeader) == "" { + writePlainTextResponse(v1beta1Flagz(componentName, reg.reader), serializer, w, reg) + return + } + + mediaType, serializerInfo, err := negotiation.NegotiateOutputMediaType(r, serializer, restrictions) + if err != nil { + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + return + } + + switch serializerInfo.MediaType { + case "application/json", "application/yaml", "application/cbor": + if mediaType.Convert == nil { + err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for %s", serializerInfo.MediaType) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + return + } + group = mediaType.Convert.Group + version = mediaType.Convert.Version + deprecated = reg.deprecatedVersions()[version] + if deprecated { + w.Header().Set("Warning", `299 - "This version of the flagz endpoint is deprecated. Please use a newer version."`) + } + handleStructuredResponse(w, r, componentName, reg, serializer, restrictions, mediaType) + case "text/plain": + writePlainTextResponse(v1beta1Flagz(componentName, reg.reader), serializer, w, reg) + default: + err := fmt.Errorf("unsupported media type: %s/%s", serializerInfo.MediaType, serializerInfo.MediaTypeSubType) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + } + } +} + +func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter, reg *registry) { + reg.cachedPlainTextResponseLock.Lock() + defer reg.cachedPlainTextResponseLock.Unlock() + if reg.cachedPlainTextResponse != nil { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if _, err := w.Write(reg.cachedPlainTextResponse); err != nil { + utilruntime.HandleError(fmt.Errorf("error writing cached flagz as text/plain: %w", err)) + } + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + var textSerializer runtime.Serializer + for _, info := range serializer.SupportedMediaTypes() { + if info.MediaType == "text/plain" { + textSerializer = info.Serializer + break + } + } + if textSerializer == nil { + utilruntime.HandleError(fmt.Errorf("text/plain serializer not available")) + w.WriteHeader(http.StatusInternalServerError) + return + } + + var buf strings.Builder + if err := textSerializer.Encode(obj, &buf); err != nil { + utilruntime.HandleError(fmt.Errorf("error encoding flagz as text/plain: %w", err)) + w.WriteHeader(http.StatusInternalServerError) + return + } + reg.cachedPlainTextResponse = []byte(buf.String()) + if _, err := w.Write(reg.cachedPlainTextResponse); err != nil { + utilruntime.HandleError(fmt.Errorf("error writing flagz as text/plain: %w", err)) + } +} + +func writeStructuredResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.FlagzEndpointRestrictions, w http.ResponseWriter, r *http.Request) { + responsewriters.WriteObjectNegotiated( + serializer, + restrictions, + targetGV, + w, + r, + http.StatusOK, + obj, + true, + ) +} + +func handleStructuredResponse(w http.ResponseWriter, r *http.Request, componentName string, reg *registry, serializer runtime.NegotiatedSerializer, restrictions negotiate.FlagzEndpointRestrictions, mediaType negotiation.MediaTypeOptions) { + switch *mediaType.Convert { + case v1alpha1FlagzKind: + writeStructuredResponse(v1alpha1Flagz(componentName, reg.reader), serializer, v1alpha1FlagzKind.GroupVersion(), restrictions, w, r) + case v1beta1FlagzKind: + writeStructuredResponse(v1beta1Flagz(componentName, reg.reader), serializer, v1beta1FlagzKind.GroupVersion(), restrictions, w, r) + default: + err := fmt.Errorf("unsupported media type: %s", mediaType.Convert.String()) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + } +} + +func v1alpha1Flagz(componentName string, flagReader Reader) *v1alpha1.Flagz { + flags := flagReader.GetFlagz() + return &v1alpha1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1FlagzKind.Kind, + APIVersion: v1alpha1FlagzKind.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: componentName, + }, + Flags: flags, + } +} + +func v1beta1Flagz(componentName string, flagReader Reader) *v1beta1.Flagz { + flags := flagReader.GetFlagz() + return &v1beta1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: v1beta1FlagzKind.Kind, + APIVersion: v1beta1FlagzKind.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: componentName, + }, + Flags: flags, + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz_test.go new file mode 100644 index 000000000..6c34848dd --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/flagz_test.go @@ -0,0 +1,435 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flagz + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + cbordirect "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" + "k8s.io/apiserver/pkg/server/flagz/api/v1beta1" + utilfeature "k8s.io/apiserver/pkg/util/feature" + cliflag "k8s.io/component-base/cli/flag" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" + "sigs.k8s.io/yaml" +) + +const wantTmpl = ` +%s flagz +Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. +` + +func TestHandleFlagz(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + + fakeFlagName := "test-flag" + fakeFlagValue := "test-value" + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String(fakeFlagName, fakeFlagValue, "usage") + fakeReader := NamedFlagSetsReader{ + FlagSets: cliflag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": fs, + }, + }, + } + + tests := []struct { + name string + acceptHeader string + componentName string + registry *registry + wantStatusCode int + wantBody string + wantStructuredBody interface{} + wantWarning bool + }{ + { + name: "valid request for text/plain", + acceptHeader: "text/plain", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + ), + }, + { + name: "valid request for application/json", + acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Flagz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + }, + { + name: "deprecated version request", + acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{"v1alpha1": true}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Flagz", + APIVersion: "config.k8s.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + wantWarning: true, + }, + { + name: "valid request for application/yaml", + acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Flagz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + }, + { + name: "valid request for application/cbor", + acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Flagz", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Flagz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Flagz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + Flags: map[string]string{ + fakeFlagName: fakeFlagValue, + }, + }, + }, + { + name: "no accept header falls back to text/plain", + acceptHeader: "", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + ), + }, + { + name: "wildcard accept header falls back to text/plain", + acceptHeader: "*/*", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + ), + }, + { + name: "bad json header falls back to text/plain with wildcard", + acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Flagz,*/*", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + ), + }, + { + name: "unsupported accept header", + acceptHeader: "application/xml", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "unsupported application/json without params", + acceptHeader: "application/json", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "unsupported application/json with missing params", + acceptHeader: "application/json;v=v1beta1;g=config.k8s.io", + componentName: "test-server", + registry: ®istry{ + reader: fakeReader, + deprecatedVersionsMap: map[string]bool{}, + }, + wantStatusCode: http.StatusNotAcceptable, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mux := http.NewServeMux() + var opts []Option + var capturedReg *registry + opts = append(opts, func(reg *registry) { + capturedReg = reg + if tt.registry != nil { + reg.deprecatedVersionsMap = tt.registry.deprecatedVersionsMap + } + }) + Install(mux, tt.componentName, fakeReader, opts...) + // Assign the captured registry to tt.registry for consistency with existing test logic + tt.registry = capturedReg + + path := "/flagz" + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil) + if err != nil { + t.Fatalf("unexpected error while creating request: %v", err) + } + if tt.acceptHeader != "" { + req.Header.Set("Accept", tt.acceptHeader) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != tt.wantStatusCode { + t.Fatalf("want status code: %v, got: %v", tt.wantStatusCode, w.Code) + } + + if tt.wantStatusCode == http.StatusOK { + if tt.wantStructuredBody != nil { + var got interface{} + switch tt.wantStructuredBody.(type) { + case *v1alpha1.Flagz: + got = &v1alpha1.Flagz{} + case *v1beta1.Flagz: + got = &v1beta1.Flagz{} + default: + t.Fatalf("unexpected type for wantStructuredBody: %T", tt.wantStructuredBody) + } + unmarshalResponse(t, w.Header().Get("Content-Type"), w.Body.Bytes(), got) + + if diff := cmp.Diff(tt.wantStructuredBody, got); diff != "" { + t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) + } + if tt.wantWarning { + if !strings.Contains(w.Header().Get("Warning"), "deprecated") { + t.Errorf("expected deprecation warning in header, but got: %s", w.Header().Get("Warning")) + } + } + } else if !strings.Contains(string(w.Body.String()), tt.wantBody) { + t.Errorf("Unexpected response body:\n- want: %s\n- got: %s", tt.wantBody, string(w.Body.String())) + } + } + }) + } +} + +func unmarshalResponse(t *testing.T, contentType string, body []byte, got interface{}) { + t.Helper() + switch { + case strings.Contains(contentType, "application/json"): + if err := json.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling JSON response: %v", err) + } + case strings.Contains(contentType, "application/yaml"): + if err := yaml.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling YAML response: %v", err) + } + case strings.Contains(contentType, "application/cbor"): + if err := cbordirect.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling CBOR response: %v", err) + } + default: + t.Fatalf("unexpected content type: %s", contentType) + } +} + +func TestCache(t *testing.T) { + fakeFlagName := "test-flag" + fakeFlagValue := "test-value" + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String(fakeFlagName, fakeFlagValue, "usage") + fakeReader := NamedFlagSetsReader{ + FlagSets: cliflag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": fs, + }, + }, + } + mux := http.NewServeMux() + var capturedReg *registry + Install(mux, "test-server", fakeReader, func(reg *registry) { + capturedReg = reg + }) + + path := "/flagz" + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil) + if err != nil { + t.Fatalf("unexpected error while creating request: %v", err) + } + req.Header.Set("Accept", "text/plain") + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code) + } + if capturedReg.cachedPlainTextResponse == nil { + t.Fatalf("cached response should not be nil") + } + cached := capturedReg.cachedPlainTextResponse + fs.String("new-flag", "new-value", "usage") + + w = httptest.NewRecorder() + mux.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("want status code: %v, got: %v", http.StatusOK, w.Code) + } + if diff := cmp.Diff(cached, capturedReg.cachedPlainTextResponse); diff != "" { + t.Errorf("Unexpected diff on cached response (-want,+got):\n%s", diff) + } +} + +// TestNewFlagzCodecFactory ensures all media types in the codec factory +// are explicitly handled. If this test fails, a new media type was added +// to the codec factory and needs to be explicitly added to the supported +// or unsupported list in newFlagzCodecFactory. +func TestNewFlagzCodecFactory(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) + + _, err := newFlagzCodecFactory(scheme, "", nil) + if err != nil { + t.Fatalf("unknown media type(s) detected - update newFlagzCodecFactory to explicitly handle them: %v", err) + } +} + +func TestMetrics(t *testing.T) { + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String("test-flag", "test-value", "usage") + fakeReader := NamedFlagSetsReader{ + FlagSets: cliflag.NamedFlagSets{ + FlagSets: map[string]*pflag.FlagSet{ + "test": fs, + }, + }, + } + + mux := http.NewServeMux() + Install(mux, "test-server", fakeReader) + metrics.Register() + metrics.Reset() + + // text/plain request: group and version should be empty + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil) + if err != nil { + t.Errorf("%v", err) + } + mux.ServeHTTP(httptest.NewRecorder(), req) + + // structured request: group and version should reflect the negotiated version + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultFlagzPath), nil) + if err != nil { + t.Errorf("%v", err) + } + req.Header.Set("Accept", "application/json;v=v1alpha1;g=config.k8s.io;as=Flagz") + mux.ServeHTTP(httptest.NewRecorder(), req) + + expected := strings.NewReader(` + # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code. + # TYPE apiserver_request_total counter + apiserver_request_total{code="200",component="test-server",dry_run="",group="",resource="flagz",scope="",subresource="",verb="GET",version=""} 1 + apiserver_request_total{code="200",component="test-server",dry_run="",group="config.k8s.io",resource="flagz",scope="",subresource="",verb="GET",version="v1alpha1"} 1 +`) + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil { + t.Error(err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/negotiate/negotiation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/negotiate/negotiation.go new file mode 100644 index 000000000..ec2d28e67 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/negotiate/negotiation.go @@ -0,0 +1,46 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package negotiate + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// FlagzEndpointRestrictions implements content negotiation restrictions for the flagz endpoint. +// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses. +type FlagzEndpointRestrictions struct { + RecognizedStructuredKinds map[schema.GroupVersionKind]bool +} + +// AllowsMediaTypeTransform checks if the provided GVK is supported for structured flagz responses. +func (f FlagzEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool { + if mimeType == "text" && mimeSubType == "plain" { + return gvk == nil + } + if gvk != nil { + return f.RecognizedStructuredKinds[*gvk] + } + return false +} + +func (FlagzEndpointRestrictions) AllowsServerVersion(string) bool { + return false +} + +func (FlagzEndpointRestrictions) AllowsStreamSchema(s string) bool { + return false +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/registry.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/registry.go new file mode 100644 index 000000000..7fe489a18 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/registry.go @@ -0,0 +1,39 @@ +/* +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 flagz + +import ( + "sync" +) + +type registry struct { + // reader is a Reader where we can get the flags. + reader Reader + // deprecatedVersionsMap is a map of deprecated flagz versions. + deprecatedVersionsMap map[string]bool + // cachedPlainTextResponse is a cached response of the flagz endpoint. + cachedPlainTextResponse []byte + // cachedPlainTextResponseLock is a lock for the cachedPlainTextResponse. + cachedPlainTextResponseLock sync.Mutex +} + +// Option is a function to configure registry. +type Option func(reg *registry) + +func (r *registry) deprecatedVersions() map[string]bool { + return r.deprecatedVersionsMap +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/testing/testing.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/testing/testing.go new file mode 100644 index 000000000..8b3edd65b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/testing/testing.go @@ -0,0 +1,120 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "encoding/json" + "strings" + "testing" + + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/server/flagz/api/v1alpha1" + "k8s.io/apiserver/pkg/server/flagz/api/v1beta1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func VerifyStructuredResponse(t *testing.T, acceptHeader string, body []byte, warnings []string, want interface{}, wantDeprecationHeader bool) { + t.Helper() + + unmarshal := unmarshalFunc(t, acceptHeader) + wantTypeMeta, wantName, wantFlags := wantFields(t, want) + gotTypeMeta, gotName, gotFlags := gotFields(t, unmarshal, body, wantTypeMeta.APIVersion) + + if gotName != wantName { + t.Errorf("name mismatch: got %q, want %q", gotName, wantName) + } + if gotTypeMeta != wantTypeMeta { + t.Errorf("type meta mismatch: got %v, want %v", gotTypeMeta, wantTypeMeta) + } + for k, v := range wantFlags { + gotV, ok := gotFlags[k] + if !ok { + t.Errorf("missing flag %q", k) + continue + } + if gotV != v { + t.Errorf("flag %q match: got %q, want %q", k, gotV, v) + } + } + + foundWarning := false + for _, w := range warnings { + if strings.Contains(w, "deprecated") { + foundWarning = true + break + } + } + if foundWarning != wantDeprecationHeader { + t.Errorf("deprecation header mismatch: got %v, want %v", foundWarning, wantDeprecationHeader) + } +} + +func unmarshalFunc(t *testing.T, acceptHeader string) func([]byte, interface{}) error { + switch { + case strings.Contains(acceptHeader, "application/json"): + return json.Unmarshal + case strings.Contains(acceptHeader, "application/yaml"): + return yaml.Unmarshal + case strings.Contains(acceptHeader, "application/cbor"): + return cbor.Unmarshal + default: + t.Fatalf("unexpected Accept header: %q", acceptHeader) + } + return nil +} + +func wantFields(t *testing.T, want interface{}) (metav1.TypeMeta, string, map[string]string) { + t.Helper() + switch w := want.(type) { + case *v1alpha1.Flagz: + return w.TypeMeta, w.Name, w.Flags + case *v1beta1.Flagz: + return w.TypeMeta, w.Name, w.Flags + default: + t.Fatalf("unexpected type for want: %T", want) + return metav1.TypeMeta{}, "", nil + } +} + +func gotFields(t *testing.T, unmarshal func([]byte, interface{}) error, body []byte, apiVersion string) (metav1.TypeMeta, string, map[string]string) { + var gotName string + var gotTypeMeta metav1.TypeMeta + var gotFlags map[string]string + switch apiVersion { + case "config.k8s.io/v1alpha1": + var got v1alpha1.Flagz + if err := unmarshal(body, &got); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + gotName = got.Name + gotTypeMeta = got.TypeMeta + gotFlags = got.Flags + case "config.k8s.io/v1beta1": + var got v1beta1.Flagz + if err := unmarshal(body, &got); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + gotName = got.Name + gotTypeMeta = got.TypeMeta + gotFlags = got.Flags + default: + t.Fatalf("unexpected API version: %q", apiVersion) + } + return gotTypeMeta, gotName, gotFlags +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/textserializer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/textserializer.go new file mode 100644 index 000000000..ee9f72edd --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/flagz/textserializer.go @@ -0,0 +1,77 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package flagz + +import ( + "fmt" + "io" + "math/rand" + "sort" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + delimiters = []string{":", ": ", "=", " "} +) + +const headerFmt = ` +%s flagz +Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. +` + +// flagzTextSerializer implements runtime.Serializer for text/plain output. +type flagzTextSerializer struct { + componentName string + flagReader Reader +} + +// Encode writes the flagz information in plain text format to the given writer, using the provided obj. +func (s flagzTextSerializer) Encode(obj runtime.Object, w io.Writer) error { + if _, err := fmt.Fprintf(w, headerFmt, s.componentName); err != nil { + return err + } + + randomIndex := rand.Intn(len(delimiters)) + separator := delimiters[randomIndex] + + flags := s.flagReader.GetFlagz() + var sortedKeys []string + for key := range flags { + sortedKeys = append(sortedKeys, key) + } + + sort.Strings(sortedKeys) + for _, key := range sortedKeys { + if _, err := fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key]); err != nil { + return err + } + } + + return nil +} + +// Decode is not supported for text/plain serialization. +func (s flagzTextSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + return nil, nil, fmt.Errorf("decode not supported for text/plain") +} + +// Identifier returns a unique identifier for this serializer. +func (s flagzTextSerializer) Identifier() runtime.Identifier { + return runtime.Identifier("flagzTextSerializer") +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver.go index 4d0034634..715501f67 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver.go @@ -49,15 +49,16 @@ import ( discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/apiserver/pkg/server/flagz" "k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/routes" + "k8s.io/apiserver/pkg/server/statusz" "k8s.io/apiserver/pkg/storageversion" utilfeature "k8s.io/apiserver/pkg/util/feature" restclient "k8s.io/client-go/rest" basecompatibility "k8s.io/component-base/compatibility" "k8s.io/component-base/featuregate" zpagesfeatures "k8s.io/component-base/zpages/features" - "k8s.io/component-base/zpages/statusz" "k8s.io/klog/v2" openapibuilder3 "k8s.io/kube-openapi/pkg/builder3" openapicommon "k8s.io/kube-openapi/pkg/common" @@ -113,6 +114,9 @@ type GenericAPIServer struct { // LoopbackClientConfig is a config for a privileged loopback connection to the API server LoopbackClientConfig *restclient.Config + // Flagz is used to set up flagz endpoint. + Flagz flagz.Reader + // minRequestTimeout is how short the request timeout can be. This is used to build the RESTHandler minRequestTimeout time.Duration @@ -155,6 +159,9 @@ type GenericAPIServer struct { // AggregatedDiscoveryGroupManager serves /apis in an aggregated form. AggregatedDiscoveryGroupManager discoveryendpoint.ResourceManager + // PeerAggregatedDiscoveryManager serves /apis aggregated from all peer apiservers. + PeerAggregatedDiscoveryManager discoveryendpoint.PeerAggregatedResourceManager + // AggregatedLegacyDiscoveryGroupManager serves /api in an aggregated form. AggregatedLegacyDiscoveryGroupManager discoveryendpoint.ResourceManager @@ -460,8 +467,15 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer { } s.installReadyz() + componentName := "apiserver" + if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentFlagz) { + if s.Flagz != nil { + flagz.Install(s.Handler.NonGoRestfulMux, componentName, s.Flagz) + } + } + // statusz is installed last so that it can list all the paths that have been registered if utilfeature.DefaultFeatureGate.Enabled(zpagesfeatures.ComponentStatusz) { - statusz.Install(s.Handler.NonGoRestfulMux, "apiserver", statusz.NewRegistry(s.EffectiveVersion, statusz.WithListedPaths(s.ListedPaths()))) + statusz.Install(s.Handler.NonGoRestfulMux, componentName, statusz.NewRegistry(s.EffectiveVersion, statusz.WithListedPaths(s.ListedPaths()))) } return preparedGenericAPIServer{s} @@ -530,8 +544,8 @@ func (s preparedGenericAPIServer) RunWithContext(ctx context.Context) error { // If UDS profiling is enabled, start a local http server listening on that socket if s.UnprotectedDebugSocket != nil { go func() { - defer utilruntime.HandleCrash() - klog.Error(s.UnprotectedDebugSocket.Run(stopCh)) + defer utilruntime.HandleCrashWithContext(ctx) + klog.Error(s.UnprotectedDebugSocket.RunWithContext(ctx)) }() } @@ -859,7 +873,8 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo // Install the version handler. // Add a handler at / to enumerate the supported api versions. legacyRootAPIHandler := discovery.NewLegacyRootAPIHandler(s.discoveryAddresses, s.Serializer, apiPrefix) - wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(legacyRootAPIHandler, s.AggregatedLegacyDiscoveryGroupManager) + // No peer-to-peer discovery for legacy API group. + wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(legacyRootAPIHandler, s.AggregatedLegacyDiscoveryGroupManager, s.AggregatedLegacyDiscoveryGroupManager) s.Handler.GoRestfulContainer.Add(wrapped.GenerateWebService("/api", metav1.APIVersions{})) s.registerStorageReadinessCheck("", apiGroupInfo) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_graceful_termination_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_graceful_termination_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_graceful_termination_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_graceful_termination_test.go index 39079c616..24d918ebf 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_graceful_termination_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_graceful_termination_test.go @@ -39,10 +39,13 @@ import ( auditinternal "k8s.io/apiserver/pkg/apis/audit" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/openapi" apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/server/dynamiccertificates" + clientscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/klog/v2" "k8s.io/klog/v2/ktesting" + kubeopenapi "k8s.io/kube-openapi/pkg/common" "github.com/google/go-cmp/cmp" "golang.org/x/net/http2" @@ -1024,6 +1027,12 @@ func newClient(useNewConnection bool) *http.Client { } } +func getOpenAPIDefinitionsForTest(_ kubeopenapi.ReferenceCallback) map[string]kubeopenapi.OpenAPIDefinition { + return map[string]kubeopenapi.OpenAPIDefinition{ + "io.k8s.apimachinery.pkg.apis.meta.v1.APIGroupList": {}, + } +} + func newGenericAPIServer(t *testing.T, fAudit *fakeAudit, keepListening bool) *GenericAPIServer { config, _ := setUp(t) config.ShutdownDelayDuration = 100 * time.Millisecond @@ -1032,6 +1041,9 @@ func newGenericAPIServer(t *testing.T, fAudit *fakeAudit, keepListening bool) *G config.ShutdownWatchTerminationGracePeriod = 2 * time.Second config.AuditPolicyRuleEvaluator = fAudit config.AuditBackend = fAudit + namer := openapi.NewDefinitionNamer(clientscheme.Scheme) + config.OpenAPIConfig = DefaultOpenAPIConfig(getOpenAPIDefinitionsForTest, namer) + config.OpenAPIV3Config = DefaultOpenAPIV3Config(getOpenAPIDefinitionsForTest, namer) s, err := config.Complete(nil).New("test", NewEmptyDelegate()) if err != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_test.go index d3457cf34..7c60c7d8b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/genericapiserver_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/genericapiserver_test.go @@ -119,11 +119,11 @@ func buildTestOpenAPIDefinition() kubeopenapi.OpenAPIDefinition { func testGetOpenAPIDefinitions(_ kubeopenapi.ReferenceCallback) map[string]kubeopenapi.OpenAPIDefinition { return map[string]kubeopenapi.OpenAPIDefinition{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Status": {}, - "k8s.io/apimachinery/pkg/apis/meta/v1.APIVersions": {}, - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": {}, - "k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": buildTestOpenAPIDefinition(), - "k8s.io/apimachinery/pkg/apis/meta/v1.APIResourceList": {}, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": {}, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIVersions": {}, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIGroupList": {}, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup": buildTestOpenAPIDefinition(), + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": {}, } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/graceful_shutdown_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/graceful_shutdown_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/graceful_shutdown_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/graceful_shutdown_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/handler.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/handler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/handler.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/healthz.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/healthz.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/healthz.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/healthz.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/healthz_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/healthz_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz/healthz_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz/healthz_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/healthz_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/healthz_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/hooks.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/hooks.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/hooks.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/hooks.go index 150b40b47..3a828061e 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/hooks.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/hooks.go @@ -195,8 +195,7 @@ func (s *GenericAPIServer) isPostStartHookRegistered(name string) bool { func runPostStartHook(name string, entry postStartHookEntry, context PostStartHookContext) { var err error func() { - // don't let the hook *accidentally* panic and kill the server - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(context) err = entry.hook(context) }() // if the hook intentionally wants to kill server, let it. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/httplog.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/httplog.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/httplog.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/httplog.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/httplog_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/httplog_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/httplog/httplog_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/httplog/httplog_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/lifecycle_signals.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/lifecycle_signals.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/lifecycle_signals.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/lifecycle_signals.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/pathrecorder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/pathrecorder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/pathrecorder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/pathrecorder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/pathrecorder_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/pathrecorder_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/mux/pathrecorder_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/mux/pathrecorder_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/admission.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/admission.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/admission.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/admission.go index 6b4669e45..2a8c8fd9c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/admission.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/admission.go @@ -44,6 +44,7 @@ import ( "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/restmapper" + "k8s.io/component-base/compatibility" "k8s.io/component-base/featuregate" ) @@ -130,6 +131,7 @@ func (a *AdmissionOptions) ApplyTo( kubeClient kubernetes.Interface, dynamicClient dynamic.Interface, features featuregate.FeatureGate, + effectiveVersion compatibility.EffectiveVersion, pluginInitializers ...admission.PluginInitializer, ) error { if a == nil { @@ -154,8 +156,8 @@ func (a *AdmissionOptions) ApplyTo( discoveryClient := cacheddiscovery.NewMemCacheClient(kubeClient.Discovery()) discoveryRESTMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) genericInitializer := initializer.New(kubeClient, dynamicClient, informers, c.Authorization.Authorizer, features, - c.DrainedNotify(), discoveryRESTMapper) - initializersChain := admission.PluginInitializers{genericInitializer} + effectiveVersion, c.DrainedNotify(), discoveryRESTMapper) + initializersChain := admission.PluginInitializers{initializer.NewAPIServerIDInitializer(c.APIServerID), genericInitializer} initializersChain = append(initializersChain, pluginInitializers...) admissionPostStartHook := func(hookContext server.PostStartHookContext) error { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/admission_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/admission_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/admission_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/admission_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement.go similarity index 70% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement.go index 44a5ddcc5..6698c73a2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/api_enablement.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/resourceconfig" serverstore "k8s.io/apiserver/pkg/server/storage" @@ -70,20 +71,22 @@ func (s *APIEnablementOptions) Validate(registries ...GroupRegistry) []error { errors := []error{} if s.RuntimeConfig[resourceconfig.APIAll] == "false" && len(s.RuntimeConfig) == 1 { // Do not allow only set api/all=false, in such case apiserver startup has no meaning. - return append(errors, fmt.Errorf("invalid key with only %v=false", resourceconfig.APIAll)) + return append(errors, fmt.Errorf("invalid runtime-config with only %v=false", resourceconfig.APIAll)) } - groups, err := resourceconfig.ParseGroups(s.RuntimeConfig) + groupVersions, err := resourceconfig.ParseGroupVersions(s.RuntimeConfig) if err != nil { return append(errors, err) } - for _, registry := range registries { - // filter out known groups - groups = unknownGroups(groups, registry) + unknownGroupVersions := sets.New[string]() + for _, groupVersion := range groupVersions { + if !isGroupRegistered(groupVersion.Group, registries) { + unknownGroupVersions.Insert(groupVersion.String()) + } } - if len(groups) != 0 { - errors = append(errors, fmt.Errorf("unknown api groups %s", strings.Join(groups, ","))) + if len(unknownGroupVersions) != 0 { + errors = append(errors, fmt.Errorf("unknown api groups %s", strings.Join(sets.List(unknownGroupVersions), ","))) } return errors @@ -106,10 +109,22 @@ func (s *APIEnablementOptions) ApplyTo(c *server.Config, defaultResourceConfig * c.MergedResourceConfig = mergedResourceConfig - if binVersion, emulatedVersion := c.EffectiveVersion.BinaryVersion(), c.EffectiveVersion.EmulationVersion(); !binVersion.EqualTo(emulatedVersion) { + binVersion, emulatedVersion := c.EffectiveVersion.BinaryVersion(), c.EffectiveVersion.EmulationVersion() + if binVersion != nil && emulatedVersion != nil && (binVersion.Major() != emulatedVersion.Major() || binVersion.Minor() != emulatedVersion.Minor()) { for _, version := range registry.PrioritizedVersionsAllGroups() { if strings.Contains(version.Version, "alpha") { - klog.Warningf("alpha api enabled with emulated version %s instead of the binary's version %s, this is unsupported, proceed at your own risk: api=%s", emulatedVersion, binVersion, version.String()) + // Check if this alpha API is actually enabled before warning + entireVersionEnabled := c.MergedResourceConfig.ExplicitGroupVersionConfigs[version] + individualResourceEnabled := false + for resource, enabled := range c.MergedResourceConfig.ExplicitResourceConfigs { + if enabled && resource.Group == version.Group && resource.Version == version.Version { + individualResourceEnabled = true + break + } + } + if entireVersionEnabled || individualResourceEnabled { + klog.Warningf("alpha api enabled with emulated version %s instead of the binary's version %s, this is unsupported, proceed at your own risk: api=%s", emulatedVersion, binVersion, version.String()) + } } } } @@ -117,18 +132,17 @@ func (s *APIEnablementOptions) ApplyTo(c *server.Config, defaultResourceConfig * return err } -func unknownGroups(groups []string, registry GroupRegistry) []string { - unknownGroups := []string{} - for _, group := range groups { - if !registry.IsGroupRegistered(group) { - unknownGroups = append(unknownGroups, group) - } - } - return unknownGroups -} - // GroupRegistry provides a method to check whether given group is registered. type GroupRegistry interface { - // IsRegistered returns true if given group is registered. + // IsGroupRegistered returns true if given group is registered. IsGroupRegistered(group string) bool } + +func isGroupRegistered(group string, registries []GroupRegistry) bool { + for _, registry := range registries { + if registry.IsGroupRegistered(group) { + return true + } + } + return false +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement_test.go new file mode 100644 index 000000000..06978f9ff --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/api_enablement_test.go @@ -0,0 +1,380 @@ +/* +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 options + +import ( + "bytes" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/version" + apimachineryversion "k8s.io/apimachinery/pkg/version" + "k8s.io/apiserver/pkg/server" + serverstore "k8s.io/apiserver/pkg/server/storage" + cliflag "k8s.io/component-base/cli/flag" + "k8s.io/component-base/compatibility" + "k8s.io/klog/v2" +) + +type fakeGroupRegistry struct{} + +func (f fakeGroupRegistry) IsGroupRegistered(group string) bool { + return group == "apiregistration.k8s.io" +} + +func TestAPIEnablementOptionsValidate(t *testing.T) { + testCases := []struct { + name string + runtimeConfig cliflag.ConfigurationMap + expectErr string + }{ + { + name: "test when options is nil", + }, + { + name: "test when invalid runtime-config with only api/all=false", + runtimeConfig: cliflag.ConfigurationMap{"api/all": "false"}, + expectErr: "invalid runtime-config with only api/all=false", + }, + { + name: "test when ConfigurationMap key is invalid", + runtimeConfig: cliflag.ConfigurationMap{"apiall": "false"}, + expectErr: "runtime-config invalid key", + }, + { + name: "test when unknown api groups", + runtimeConfig: cliflag.ConfigurationMap{"api/v1": "true", "api/v1beta2": "true"}, + expectErr: "unknown api groups api/v1,api/v1beta2", + }, + { + name: "test when valid api groups", + runtimeConfig: cliflag.ConfigurationMap{"apiregistration.k8s.io/v1beta1": "true"}, + }, + { + name: "test when invalid api groups", + runtimeConfig: cliflag.ConfigurationMap{"apiregistration.k8s.io/v1beta1": "true"}, + }, + } + testGroupRegistry := fakeGroupRegistry{} + + for _, testcase := range testCases { + t.Run(testcase.name, func(t *testing.T) { + testOptions := &APIEnablementOptions{ + RuntimeConfig: testcase.runtimeConfig, + } + errs := testOptions.Validate(testGroupRegistry) + if len(testcase.expectErr) != 0 && !strings.Contains(utilerrors.NewAggregate(errs).Error(), testcase.expectErr) { + t.Errorf("got err: %v, expected err: %s", errs, testcase.expectErr) + } + + if len(testcase.expectErr) == 0 && len(errs) != 0 { + t.Errorf("got err: %s, expected err nil", errs) + } + }) + } +} + +type fakeGroupVersionRegistry struct { + versions []schema.GroupVersion +} + +func (f fakeGroupVersionRegistry) PrioritizedVersionsAllGroups() []schema.GroupVersion { + return f.versions +} + +func (f fakeGroupVersionRegistry) PrioritizedVersionsForGroup(group string) []schema.GroupVersion { + var result []schema.GroupVersion + for _, gv := range f.versions { + if gv.Group == group { + result = append(result, gv) + } + } + return result +} + +func (f fakeGroupVersionRegistry) IsGroupRegistered(group string) bool { + for _, gv := range f.versions { + if gv.Group == group { + return true + } + } + return false +} + +func (f fakeGroupVersionRegistry) IsVersionRegistered(gv schema.GroupVersion) bool { + for _, version := range f.versions { + if version == gv { + return true + } + } + return false +} + +func (f fakeGroupVersionRegistry) GroupVersions() []schema.GroupVersion { + return f.versions +} + +type fakeEffectiveVersion struct { + binaryVersion *version.Version + emulationVersion *version.Version +} + +func (f fakeEffectiveVersion) BinaryVersion() *version.Version { + return f.binaryVersion +} + +func (f fakeEffectiveVersion) EmulationVersion() *version.Version { + return f.emulationVersion +} + +func (f fakeEffectiveVersion) MinCompatibilityVersion() *version.Version { + return nil +} + +func (f fakeEffectiveVersion) EqualTo(other compatibility.EffectiveVersion) bool { + return f.binaryVersion.EqualTo(other.BinaryVersion()) && f.emulationVersion.EqualTo(other.EmulationVersion()) +} + +func (f fakeEffectiveVersion) String() string { + return "fake" +} + +func (f fakeEffectiveVersion) Info() *apimachineryversion.Info { + return nil +} + +func (f fakeEffectiveVersion) AllowedEmulationVersionRange() string { + return "fake range" +} + +func (f fakeEffectiveVersion) AllowedMinCompatibilityVersionRange() string { + return "fake range" +} + +func (f fakeEffectiveVersion) Validate() []error { + return nil +} + +func TestAPIEnablementOptionsApplyToVersionComparison(t *testing.T) { + // Helper function to capture klog output + captureKlogOutput := func(fn func()) string { + var buf bytes.Buffer + klog.SetOutput(&buf) + klog.LogToStderr(false) + defer func() { + klog.SetOutput(nil) + klog.LogToStderr(true) + }() + + fn() + klog.Flush() + return buf.String() + } + + testCases := []struct { + name string + binaryVersion string + emulationVersion string + alphaAPIsPresent bool + versionEnabled bool + expectWarning bool + expectWarningContent string + }{ + { + name: "same major.minor versions, different patch - no warning", + binaryVersion: "1.34.1", + emulationVersion: "1.34.0", + alphaAPIsPresent: true, + versionEnabled: true, + expectWarning: false, + }, + { + name: "same major.minor versions, no patch in emulation - no warning", + binaryVersion: "1.34.1", + emulationVersion: "1.34", + alphaAPIsPresent: true, + versionEnabled: true, + expectWarning: false, + }, + { + name: "identical versions - no warning", + binaryVersion: "1.34.1", + emulationVersion: "1.34.1", + alphaAPIsPresent: true, + versionEnabled: true, + expectWarning: false, + }, + { + name: "different major versions but not enabled - should not warn", + binaryVersion: "1.34.1", + emulationVersion: "1.33.0", + alphaAPIsPresent: true, + expectWarning: false, + }, + { + name: "different major versions - should warn", + binaryVersion: "1.34.1", + emulationVersion: "1.33.0", + alphaAPIsPresent: true, + expectWarning: true, + versionEnabled: true, + expectWarningContent: "alpha api enabled with emulated version", + }, + { + name: "different minor versions - should warn", + binaryVersion: "1.34.1", + emulationVersion: "1.33.5", + alphaAPIsPresent: true, + expectWarning: true, + versionEnabled: true, + expectWarningContent: "alpha api enabled with emulated version", + }, + { + name: "different major.minor but no alpha APIs - no warning", + binaryVersion: "1.34.1", + emulationVersion: "1.33.0", + alphaAPIsPresent: false, + expectWarning: false, + versionEnabled: true, + }, + { + name: "same major.minor with alpha APIs - no warning", + binaryVersion: "1.34.5", + emulationVersion: "1.34.0", + alphaAPIsPresent: true, + expectWarning: false, + versionEnabled: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + binaryVer := version.MustParse(tc.binaryVersion) + emulationVer := version.MustParse(tc.emulationVersion) + + effectiveVersion := fakeEffectiveVersion{ + binaryVersion: binaryVer, + emulationVersion: emulationVer, + } + + var versions []schema.GroupVersion + if tc.alphaAPIsPresent { + versions = []schema.GroupVersion{ + {Group: "rbac.authorization.k8s.io", Version: "v1alpha1"}, + {Group: "storage.k8s.io", Version: "v1alpha1"}, + } + } else { + versions = []schema.GroupVersion{ + {Group: "rbac.authorization.k8s.io", Version: "v1"}, + {Group: "storage.k8s.io", Version: "v1beta1"}, + } + } + + registry := fakeGroupVersionRegistry{versions: versions} + config := &server.Config{EffectiveVersion: effectiveVersion} + options := &APIEnablementOptions{RuntimeConfig: make(cliflag.ConfigurationMap)} + + // Enable the API + resourceConfig := serverstore.NewResourceConfig() + if tc.versionEnabled { + resourceConfig.ExplicitGroupVersionConfigs[schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1alpha1"}] = true + } + + // Capture log output during ApplyTo execution + logOutput := captureKlogOutput(func() { + err := options.ApplyTo(config, resourceConfig, registry) + if err != nil { + t.Errorf("ApplyTo failed: %v", err) + } + }) + + // Verify warning expectations + if tc.expectWarning { + if !strings.Contains(logOutput, tc.expectWarningContent) { + t.Errorf("Expected warning containing '%s', but got log output: %s", tc.expectWarningContent, logOutput) + } + if !strings.Contains(logOutput, "W") { // klog warning prefix + t.Errorf("Expected warning log level, but got log output: %s", logOutput) + } + } else if strings.Contains(logOutput, "alpha api enabled") { + t.Errorf("Expected no warning, but got log output: %s", logOutput) + } + }) + } +} + +func TestAPIEnablementOptionsApplyToErrorCases(t *testing.T) { + // Create a default effective version for test configs + defaultEffectiveVersion := fakeEffectiveVersion{ + binaryVersion: version.MustParse("1.34.0"), + emulationVersion: version.MustParse("1.34.0"), + } + + testCases := []struct { + name string + options *APIEnablementOptions + config *server.Config + expectError bool + errorContains string + }{ + { + name: "nil options should not error", + options: nil, + config: &server.Config{ + EffectiveVersion: defaultEffectiveVersion, + }, + expectError: false, + }, + { + name: "invalid runtime config value should error", + options: &APIEnablementOptions{ + RuntimeConfig: cliflag.ConfigurationMap{ + "api/all": "invalid-value", // Must be "true" or "false" + }, + }, + config: &server.Config{ + EffectiveVersion: defaultEffectiveVersion, + }, + expectError: true, + errorContains: "invalid value", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + registry := fakeGroupVersionRegistry{versions: []schema.GroupVersion{ + {Group: "rbac.authorization.k8s.io", Version: "v1"}, + }} + + err := tc.options.ApplyTo(tc.config, serverstore.NewResourceConfig(), registry) + + if tc.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) { + t.Errorf("Expected error containing '%s', but got: %v", tc.errorContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit.go index af5b06a5b..74e2e7197 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit.go @@ -154,6 +154,10 @@ func NewAuditOptions() *AuditOptions { GroupVersionString: "audit.k8s.io/v1", }, LogOptions: AuditLogOptions{ + MaxSize: 100, + MaxAge: 366, + MaxBackups: 100, + Format: pluginlog.FormatJson, BatchOptions: AuditBatchOptions{ Mode: ModeBlocking, @@ -436,11 +440,11 @@ func (o *AuditLogOptions) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.Path, "audit-log-path", o.Path, "If set, all requests coming to the apiserver will be logged to this file. '-' means standard out.") fs.IntVar(&o.MaxAge, "audit-log-maxage", o.MaxAge, - "The maximum number of days to retain old audit log files based on the timestamp encoded in their filename.") + "The maximum number of days to retain old audit log files based on the timestamp encoded in their filename. Setting a value of 0 means old audit log files are not removed based on age.") fs.IntVar(&o.MaxBackups, "audit-log-maxbackup", o.MaxBackups, "The maximum number of old audit log files to retain. Setting a value of 0 will mean there's no restriction on the number of files.") fs.IntVar(&o.MaxSize, "audit-log-maxsize", o.MaxSize, - "The maximum size in megabytes of the audit log file before it gets rotated.") + "The maximum size in megabytes of the audit log file before it gets rotated. Setting to 0 disables rotation (not recommended).") fs.StringVar(&o.Format, "audit-log-format", o.Format, "Format of saved audits. \"legacy\" indicates 1-line text format for each event."+ " \"json\" indicates structured json format. Known formats are "+ @@ -506,6 +510,10 @@ func (o *AuditLogOptions) getWriter() (io.Writer, error) { return nil, fmt.Errorf("ensureLogFile: %w", err) } + if o.MaxSize == 0 { + return os.OpenFile(o.Path, os.O_APPEND|os.O_WRONLY, 0644) + } + return &lumberjack.Logger{ Filename: o.Path, MaxAge: o.MaxAge, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit_test.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit_test.go index 78fdc7210..16425d88b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/audit_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/audit_test.go @@ -38,10 +38,10 @@ func TestAuditValidOptions(t *testing.T) { auditPath := filepath.Join(tmpDir, "audit") webhookConfig := makeTmpWebhookConfig(t) - defer os.Remove(webhookConfig) + defer func() { _ = os.Remove(webhookConfig) }() policy := makeTmpPolicy(t) - defer os.Remove(policy) + defer func() { _ = os.Remove(policy) }() testCases := []struct { name string @@ -177,14 +177,46 @@ func TestAuditValidOptions(t *testing.T) { if options.LogOptions.Path == "-" { assert.Equal(t, os.Stdout, w) assert.NoFileExists(t, options.LogOptions.Path) + } else if options.LogOptions.MaxSize == 0 { + // When MaxSize is 0, we return a raw file writer (os.File) for unlimited size + file, ok := w.(*os.File) + assert.True(t, ok, "Writer should be of type *os.File when MaxSize is 0") + assert.FileExists(t, options.LogOptions.Path) + assert.NoError(t, file.Close()) } else { - assert.IsType(t, (*lumberjack.Logger)(nil), w) + logger, ok := w.(*lumberjack.Logger) + assert.True(t, ok, "Writer should be of type *lumberjack.Logger") assert.FileExists(t, options.LogOptions.Path) + assert.Equal(t, options.LogOptions.MaxSize, logger.MaxSize) } }) } } +func TestAuditLogMaxSizeZero(t *testing.T) { + tmpDir := t.TempDir() + auditPath := filepath.Join(tmpDir, "audit") + policy := makeTmpPolicy(t) + defer func() { _ = os.Remove(policy) }() + + o := NewAuditOptions() + o.LogOptions.Path = auditPath + o.PolicyFile = policy + o.LogOptions.MaxSize = 0 + + w, err := o.LogOptions.getWriter() + require.NoError(t, err) + require.NotNil(t, w) + + // When MaxSize is 0, we return a raw file writer (os.File) for unlimited size + file, ok := w.(*os.File) + require.True(t, ok, "Should be an os.File when MaxSize is 0") + + // Verify that the file was opened correctly + require.NotNil(t, file) + require.NoError(t, file.Close()) +} + func TestAuditInvalidOptions(t *testing.T) { tmpDir := t.TempDir() auditPath := filepath.Join(tmpDir, "audit") diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication_dynamic_request_header.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication_dynamic_request_header.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication_dynamic_request_header.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication_dynamic_request_header.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authentication_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authentication_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authenticationconfig/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authenticationconfig/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authenticationconfig/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authenticationconfig/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authenticationconfig/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authenticationconfig/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authenticationconfig/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authenticationconfig/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorization.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorization.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorization.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorization.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorizationconfig/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorizationconfig/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorizationconfig/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorizationconfig/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorizationconfig/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorizationconfig/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/authorizationconfig/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/authorizationconfig/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/coreapi.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/coreapi.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/coreapi.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/coreapi.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/egress_selector.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/egress_selector.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/egress_selector.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/egress_selector.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config.go index 6d0e28428..2d481bc6f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config.go @@ -282,8 +282,6 @@ func getTransformerOverridesAndKMSPluginProbes(ctx context.Context, config *apis // For each entry in the configuration for _, resourceConfig := range config.Resources { - resourceConfig := resourceConfig - transformers, p, used, err := prefixTransformersAndProbes(ctx, resourceConfig, apiServerID) if err != nil { return nil, nil, nil, err @@ -292,7 +290,6 @@ func getTransformerOverridesAndKMSPluginProbes(ctx context.Context, config *apis // For each resource, create a list of providers to use for _, resource := range resourceConfig.Resources { - resource := resource gr := schema.ParseGroupResource(resource) // check if resource is masked by *.group rule @@ -317,8 +314,6 @@ func getTransformerOverridesAndKMSPluginProbes(ctx context.Context, config *apis transformers := make(map[schema.GroupResource]storagevalue.Transformer, len(resourceToPrefixTransformer)) for gr, transList := range resourceToPrefixTransformer { - gr := gr - transList := transList transformers[gr] = storagevalue.NewPrefixTransformers(fmt.Errorf("no matching prefix found"), transList...) } @@ -395,7 +390,7 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey // allow writes indefinitely as long as there is no error // allow writes for only up to kmsv2PluginWriteDEKSourceMaxTTL from now when there are errors // we start the timer before we make the network call because kmsv2PluginWriteDEKSourceMaxTTL is meant to be the upper bound - expirationTimestamp := envelopekmsv2.NowFunc().Add(kmsv2PluginWriteDEKSourceMaxTTL) + expirationTimestamp := envelopekmsv2.GetNowFunc(h.name)().Add(kmsv2PluginWriteDEKSourceMaxTTL) // dynamically check if we want to use KDF seed to derive DEKs or just a single DEK // this gate can only change during tests, but the check is cheap enough to always make @@ -431,6 +426,7 @@ func (h *kmsv2PluginProbe) rotateDEKOnKeyIDChange(ctx context.Context, statusKey UID: uid, ExpirationTimestamp: expirationTimestamp, CacheKey: cacheKey, + KMSProviderName: h.name, }) // it should be logically impossible for the new state to be invalid but check just in case @@ -566,7 +562,6 @@ func prefixTransformersAndProbes(ctx context.Context, config apiserver.ResourceC var kmsUsed kmsState for _, provider := range config.Providers { - provider := provider var ( transformer storagevalue.PrefixTransformer transformerErr error @@ -623,7 +618,6 @@ func aesPrefixTransformer(config *apiserver.AESConfiguration, fn blockTransforme return result, fmt.Errorf("aes provider has no valid keys") } for _, key := range config.Keys { - key := key if key.Name == "" { return result, fmt.Errorf("key with invalid name provided") } @@ -635,7 +629,6 @@ func aesPrefixTransformer(config *apiserver.AESConfiguration, fn blockTransforme keyTransformers := []storagevalue.PrefixTransformer{} for _, keyData := range config.Keys { - keyData := keyData key, err := base64.StdEncoding.DecodeString(keyData.Secret) if err != nil { return result, fmt.Errorf("could not obtain secret for named key %s: %w", keyData.Name, err) @@ -676,7 +669,6 @@ func secretboxPrefixTransformer(config *apiserver.SecretboxConfiguration) (stora return result, fmt.Errorf("secretbox provider has no valid keys") } for _, key := range config.Keys { - key := key if key.Name == "" { return result, fmt.Errorf("key with invalid name provided") } @@ -688,7 +680,6 @@ func secretboxPrefixTransformer(config *apiserver.SecretboxConfiguration) (stora keyTransformers := []storagevalue.PrefixTransformer{} for _, keyData := range config.Keys { - keyData := keyData key, err := base64.StdEncoding.DecodeString(keyData.Secret) if err != nil { return result, fmt.Errorf("could not obtain secret for named key %s: %s", keyData.Name, err) @@ -792,7 +783,9 @@ func kmsPrefixTransformer(ctx context.Context, config *apiserver.KMSConfiguratio apiServerID: apiServerID, } // initialize state so that Load always works - probe.state.Store(&envelopekmsv2.State{}) + probe.state.Store(&envelopekmsv2.State{ + KMSProviderName: kmsName, + }) primeAndProbeKMSv2(ctx, probe, kmsName) transformer := storagevalue.PrefixTransformer{ diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config_test.go index 125d43ef9..f95edbc5e 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/config_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/config_test.go @@ -1853,10 +1853,8 @@ func TestComputeEncryptionConfigHash(t *testing.T) { func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) { defaultUseSeed := GetKDF("") - origNowFunc := envelopekmsv2.NowFunc + origNowFunc := envelopekmsv2.GetNowFunc("") now := origNowFunc() // freeze time - t.Cleanup(func() { envelopekmsv2.NowFunc = origNowFunc }) - envelopekmsv2.NowFunc = func() time.Time { return now } klog.LogToStderr(false) var level klog.Level @@ -2083,6 +2081,9 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) { kmsName := fmt.Sprintf("panda-%d", i) defer SetKDFForTests(kmsName, tt.useSeed)() + resetNowFunc := envelopekmsv2.SetNowFuncForTests(kmsName, func() time.Time { return now }) + t.Cleanup(resetNowFunc) + var buf bytes.Buffer klog.SetOutput(&buf) @@ -2092,6 +2093,7 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) { name: kmsName, service: tt.service, } + tt.state.KMSProviderName = kmsName h.state.Store(&tt.state) err := h.rotateDEKOnKeyIDChange(ctx, tt.statusKeyID, "panda") @@ -2106,6 +2108,8 @@ func Test_kmsv2PluginProbe_rotateDEKOnKeyIDChange(t *testing.T) { ignoredFields := sets.NewString("Transformer", "EncryptedObjectEncryptedDEKSource", "UID", "CacheKey") gotState := *h.state.Load() + gotState.KMSProviderName = kmsName + tt.wantState.KMSProviderName = kmsName if diff := cmp.Diff(tt.wantState, gotState, cmp.FilterPath(func(path cmp.Path) bool { return ignoredFields.Has(path.String()) }, cmp.Ignore()), @@ -2225,7 +2229,6 @@ func TestGetEncryptionConfigHash(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/controller.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/controller.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/controller.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/controller.go index 5c249bcc0..9314d6d12 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/controller.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/controller.go @@ -92,7 +92,7 @@ func NewDynamicEncryptionConfiguration( // Run starts the controller and blocks until ctx is canceled. func (d *DynamicEncryptionConfigContent) Run(ctx context.Context) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) klog.InfoS("Starting controller", "name", d.name) defer klog.InfoS("Shutting down controller", "name", d.name) @@ -101,7 +101,7 @@ func (d *DynamicEncryptionConfigContent) Run(ctx context.Context) { wg.Add(1) go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer wg.Done() defer d.queue.ShutDown() <-ctx.Done() @@ -109,7 +109,7 @@ func (d *DynamicEncryptionConfigContent) Run(ctx context.Context) { wg.Add(1) go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer wg.Done() d.runWorker(ctx) }() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/controller_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/controller_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/controller_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/controller_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/testdata/ec_config.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/testdata/ec_config.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/controller/testdata/ec_config.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/controller/testdata/ec_config.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-aes-gcm.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-aes-gcm.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-aes-gcm.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-aes-gcm.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-typo.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-typo.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-typo.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/invalid-typo.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-apiversion.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-apiversion.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-apiversion.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-apiversion.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-config-type.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-config-type.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-config-type.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-config-type.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-content.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-content.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-content.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-content.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-gvk.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-gvk.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-gvk.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/invalid-configs/kms/invalid-gvk.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-cbc-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-cbc-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-cbc-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-cbc-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-gcm-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-gcm-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-gcm-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes-gcm-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/README.md b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/README.md similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/README.md rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/README.md diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys-reversed.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys-reversed.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys-reversed.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys-reversed.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-keys.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers-reversed.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers-reversed.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers-reversed.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers-reversed.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-cbc-multiple-providers.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-gcm.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-gcm.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-gcm.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/aes/aes-gcm.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/identity-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/identity-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/identity-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/identity-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/default-timeout.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/default-timeout.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/default-timeout.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/default-timeout.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-kmsv2.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-kmsv2.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-kmsv2.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-kmsv2.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-mixed.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-mixed.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-mixed.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers-mixed.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kms/multiple-providers.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kmsv2-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kmsv2-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kmsv2-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/kmsv2-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/legacy.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/legacy.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/legacy.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/legacy.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/secret-box-first.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/secret-box-first.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/encryptionconfig/testdata/valid-configs/secret-box-first.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/encryptionconfig/testdata/valid-configs/secret-box-first.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/etcd.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/etcd.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/etcd.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/etcd.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/etcd_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/etcd_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/etcd_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/etcd_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/feature.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/feature.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/feature.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/feature.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/recommended.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/recommended.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/recommended.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/recommended.go index 2ead600f8..972179778 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/recommended.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/recommended.go @@ -105,7 +105,7 @@ func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error { if err := o.Traces.ApplyTo(config.Config.EgressSelector, &config.Config); err != nil { return err } - if err := o.SecureServing.ApplyTo(&config.Config.SecureServing, &config.Config.LoopbackClientConfig); err != nil { + if err := o.SecureServing.ApplyToConfig(&config.Config); err != nil { return err } if err := o.Authentication.ApplyTo(&config.Config.Authentication, config.SecureServing, config.OpenAPIConfig); err != nil { @@ -141,6 +141,7 @@ func (o *RecommendedOptions) ApplyTo(config *server.RecommendedConfig) error { return err } if err := o.Admission.ApplyTo(&config.Config, config.SharedInformerFactory, kubeClient, dynamicClient, o.FeatureGate, + config.EffectiveVersion, initializers...); err != nil { return err } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/server_run_options.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/server_run_options.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/server_run_options.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/server_run_options.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/server_run_options_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/server_run_options_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/server_run_options_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/server_run_options_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving.go index 21a2736e1..fbcb07247 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving.go @@ -64,6 +64,14 @@ type SecureServingOptions struct { // CipherSuites is the list of allowed cipher suites for the server. // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). CipherSuites []string + // CurvePreferences is the set of allowed key exchange mechanisms for the server, + // specified as numeric Go crypto/tls CurveID values. + // The supported values depend on the Go version used. + // See https://pkg.go.dev/crypto/tls#CurveID for values supported for each Go version. + // The order of the list is ignored, and key exchange mechanisms are + // chosen by Go from this list using an internal preference order. + // If empty, the default Go curves will be used. + CurvePreferences []int32 // MinTLSVersion is the minimum TLS version supported. // Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). MinTLSVersion string @@ -189,6 +197,15 @@ func (s *SecureServingOptions) AddFlags(fs *pflag.FlagSet) { "Preferred values: "+strings.Join(tlsCipherPreferredValues, ", ")+". \n"+ "Insecure values: "+strings.Join(tlsCipherInsecureValues, ", ")+".") + fs.Int32SliceVar(&s.CurvePreferences, "tls-curve-preferences", s.CurvePreferences, + "Comma-separated list of numeric Go crypto/tls CurveID values, "+ + "as the allowed key exchange mechanisms for the server. "+ + "The supported values depend on the Go version used. "+ + "See https://pkg.go.dev/crypto/tls#CurveID for values supported for each Go version. "+ + "The order of the list is ignored, and key exchange mechanisms are chosen "+ + "by Go from this list using an internal preference order. "+ + "If omitted, the default Go curves will be used.") + tlsPossibleVersions := cliflag.TLSPossibleVersions() fs.StringVar(&s.MinTLSVersion, "tls-min-version", s.MinTLSVersion, "Minimum TLS version supported. "+ @@ -317,6 +334,14 @@ func (s *SecureServingOptions) ApplyTo(config **server.SecureServingInfo) error c.CipherSuites = cipherSuites } + if len(s.CurvePreferences) != 0 { + curvePreferences, err := cliflag.TLSCurvePreferences(s.CurvePreferences) + if err != nil { + return err + } + c.CurvePreferences = curvePreferences + } + var err error c.MinTLSVersion, err = cliflag.TLSVersion(s.MinTLSVersion) if err != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix.go index 483eac79b..6f484ac37 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2020 The Kubernetes Authors. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix_test.go index bfc481532..db23538eb 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_unix_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_unix_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2020 The Kubernetes Authors. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_windows.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_windows.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_windows.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_windows.go index 844de6188..f8fb14109 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_windows.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_windows.go @@ -1,5 +1,4 @@ //go:build windows -// +build windows /* Copyright 2020 The Kubernetes Authors. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback.go similarity index 58% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback.go index 980ddc61a..2dea7d2db 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/serving_with_loopback.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback.go @@ -18,26 +18,48 @@ package options import ( "fmt" + "net/http" "time" "github.com/google/uuid" "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/dynamiccertificates" + "k8s.io/apiserver/pkg/server/healthz" "k8s.io/client-go/rest" certutil "k8s.io/client-go/util/cert" + "k8s.io/utils/clock" ) type SecureServingOptionsWithLoopback struct { *SecureServingOptions + clock clock.PassiveClock } func (o *SecureServingOptions) WithLoopback() *SecureServingOptionsWithLoopback { - return &SecureServingOptionsWithLoopback{o} + return &SecureServingOptionsWithLoopback{ + SecureServingOptions: o, + clock: clock.RealClock{}, + } } +// Set a validity period of approximately 3 years for the loopback certificate +// to avoid kube-apiserver disruptions due to certificate expiration. +// When this certificate expires, restarting kube-apiserver will automatically +// regenerate a new certificate with fresh validity dates. +const maxAge = (3*365 + 1) * 24 * time.Hour + // ApplyTo fills up serving information in the server configuration. func (s *SecureServingOptionsWithLoopback) ApplyTo(secureServingInfo **server.SecureServingInfo, loopbackClientConfig **rest.Config) error { + return s.applyTo(secureServingInfo, loopbackClientConfig, nil) +} + +type HealthzLivezHealthChecksAdder interface { + AddHealthzChecks(checks ...healthz.HealthChecker) + AddLivezChecks(checks ...healthz.HealthChecker) +} + +func (s *SecureServingOptionsWithLoopback) applyTo(secureServingInfo **server.SecureServingInfo, loopbackClientConfig **rest.Config, healthCheckAdder HealthzLivezHealthChecksAdder) error { if s == nil || s.SecureServingOptions == nil || secureServingInfo == nil { return nil } @@ -50,12 +72,6 @@ func (s *SecureServingOptionsWithLoopback) ApplyTo(secureServingInfo **server.Se return nil } - // Set a validity period of approximately 3 years for the loopback certificate - // to avoid kube-apiserver disruptions due to certificate expiration. - // When this certificate expires, restarting kube-apiserver will automatically - // regenerate a new certificate with fresh validity dates. - maxAge := (3*365 + 1) * 24 * time.Hour - // create self-signed cert+key with the fake server.LoopbackClientServerNameOverride and // let the server return it when the loopback client connects. certPem, keyPem, err := certutil.GenerateSelfSignedCertKeyWithOptions(certutil.SelfSignedCertKeyOptions{ @@ -85,7 +101,37 @@ func (s *SecureServingOptionsWithLoopback) ApplyTo(secureServingInfo **server.Se default: *loopbackClientConfig = secureLoopbackClientConfig + if healthCheckAdder != nil { + s.addLoopbackServingCertificateHealthCheck(healthCheckAdder) + } } return nil } + +func (s *SecureServingOptionsWithLoopback) ApplyToConfig(cfg *server.Config) error { + return s.applyTo(&cfg.SecureServing, &cfg.LoopbackClientConfig, cfg) +} + +// addLoopbackServingCertificateHealthCheck adds a health check called `loopback-certificate-expiry` to the +// server that fails when the loopback client certificate has expired, enabling +// liveness probes to be used to automatically restart the apiserver. +func (s *SecureServingOptionsWithLoopback) addLoopbackServingCertificateHealthCheck(healthCheckAdder HealthzLivezHealthChecksAdder) { + expirationDate := s.clock.Now().Add(maxAge) + check := healthz.NamedCheck("loopback-serving-certificate", func(r *http.Request) error { + if s.clock.Now().After(expirationDate) { + return LoopbackCertificateExpiredError{} + } + + return nil + }) + + healthCheckAdder.AddHealthzChecks(check) + healthCheckAdder.AddLivezChecks(check) +} + +type LoopbackCertificateExpiredError struct{} + +func (lcee LoopbackCertificateExpiredError) Error() string { + return "loopback serving certificate is expired" +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback_test.go new file mode 100644 index 000000000..95bb11229 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/serving_with_loopback_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2021 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 options + +import ( + "errors" + "net" + "testing" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/server" + "k8s.io/apiserver/pkg/server/healthz" + "k8s.io/client-go/rest" + clocktesting "k8s.io/utils/clock/testing" + netutils "k8s.io/utils/net" +) + +func TestEmptyMainCert(t *testing.T) { + secureServingInfo := &server.SecureServingInfo{} + var loopbackClientConfig *rest.Config + + s := (&SecureServingOptions{ + BindAddress: netutils.ParseIPSloppy("127.0.0.1"), + }).WithLoopback() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:0") + } + defer ln.Close() + s.Listener = ln + s.BindPort = ln.Addr().(*net.TCPAddr).Port + + if err := s.ApplyTo(&secureServingInfo, &loopbackClientConfig); err != nil { + t.Errorf("unexpected error: %v", err) + } + if loopbackClientConfig == nil { + t.Errorf("unexpected empty loopbackClientConfig") + } + if e, a := 1, len(secureServingInfo.SNICerts); e != a { + t.Errorf("expected %d SNICert, got %d", e, a) + } +} + +func TestApplyToConfig(t *testing.T) { + type testcase struct { + name string + expired bool + } + + testcases := []testcase{ + { + name: "current time not after certificate expiry", + }, + { + name: "current time after certificate expiry", + expired: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + cfg := server.NewConfig(serializer.NewCodecFactory(runtime.NewScheme())) + + ssowlb := (&SecureServingOptions{ + BindAddress: netutils.ParseIPSloppy("127.0.0.1"), + }).WithLoopback() + now := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.Local) + fakeClock := clocktesting.NewFakeClock(now) + ssowlb.clock = fakeClock + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen on 127.0.0.1:0") + } + defer ln.Close() //nolint:errcheck + ssowlb.Listener = ln + ssowlb.BindPort = ln.Addr().(*net.TCPAddr).Port + + if err := ssowlb.ApplyToConfig(cfg); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if tc.expired { + fakeClock.Step(maxAge + 1) + } + + checkSet := map[string][]healthz.HealthChecker{ + "/livez": cfg.LivezChecks, + "/healthz": cfg.HealthzChecks, + } + + for endpoint, checks := range checkSet { + var foundChecker healthz.HealthChecker + + for _, check := range checks { + if check.Name() == "loopback-serving-certificate" { + foundChecker = check + break + } + } + + if foundChecker == nil { + t.Fatalf("loopback-serving-certificate health checker not found for %s endpoint", endpoint) + } + + err = foundChecker.Check(nil) + if tc.expired { + if !errors.Is(err, LoopbackCertificateExpiredError{}) { + t.Errorf("%s endpoint check expected error and received error do not match. expected: %v , received: %v", endpoint, LoopbackCertificateExpiredError{}, err) + } + return + } + + if err != nil { + t.Errorf("%s endpoint check received an unexpected error: %v", endpoint, err) + } + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/README.md b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/README.md similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/README.md rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/README.md diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/apiserver-loopback-client__/localhost__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client-expired.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client-expired.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client-expired.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client-expired.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client-valid.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client-valid.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client-valid.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client-valid.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client.config.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client.config.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client.config.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client.config.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/client.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/client.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers-with-v2.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers-with-v2.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers-with-v2.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers-with-v2.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-providers.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-v2-providers.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-v2-providers.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/multiple-kms-v2-providers.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/multiple-kms-v2-providers.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/no-kms-provider.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/no-kms-provider.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/no-kms-provider.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/no-kms-provider.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/single-kms-provider.yaml b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/single-kms-provider.yaml similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/encryption-configs/single-kms-provider.yaml rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/encryption-configs/single-kms-provider.yaml diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/generate.sh b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/generate.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/generate.sh rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/generate.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.config.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.config.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.config.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.config.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/intermediate.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/intermediate.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/apiserver-loopback-client__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__10.0.0.1,127.0.0.1/test.com__10.0.0.1/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/localhost__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost__127.0.0.1/test.com_star.test.com_/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/localhost_test.com_127.0.0.1/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/root.csr.json b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/root.csr.json similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/root.csr.json rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/root.csr.json diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/root.pem b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/root.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/root.pem rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/root.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/localhost__/cert b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/localhost__/cert similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/localhost__/cert rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/localhost__/cert diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/localhost__/key b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/localhost__/key similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/testdata/test.com__/localhost__/key rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/testdata/test.com__/localhost__/key diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/tracing.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/tracing.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/tracing.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/tracing.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/tracing_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/tracing_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/options/tracing_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/options/tracing_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/plugins.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/plugins.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/plugins.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/plugins.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/read_write_deadline_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/read_write_deadline_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/read_write_deadline_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/read_write_deadline_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/helpers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/helpers.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/helpers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/helpers.go index c546112ec..d99d8b8b2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/helpers.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/helpers.go @@ -221,24 +221,37 @@ func getRuntimeConfigValue(overrides cliflag.ConfigurationMap, apiKey string, de // ParseGroups takes in resourceConfig and returns parsed groups. func ParseGroups(resourceConfig cliflag.ConfigurationMap) ([]string, error) { + groupVersions, err := ParseGroupVersions(resourceConfig) + if err != nil { + return nil, err + } groups := []string{} + for _, gv := range groupVersions { + groups = append(groups, gv.Group) + } + return groups, nil +} + +// ParseGroupVersions takes in resourceConfig and returns parsed group versions. +func ParseGroupVersions(resourceConfig cliflag.ConfigurationMap) ([]schema.GroupVersion, error) { + groupVersions := []schema.GroupVersion{} for key := range resourceConfig { if _, ok := groupVersionMatchers[key]; ok { continue } tokens := strings.Split(key, "/") if len(tokens) != 2 && len(tokens) != 3 { - return groups, fmt.Errorf("runtime-config invalid key %s", key) + return nil, fmt.Errorf("runtime-config invalid key %s", key) } groupVersionString := tokens[0] + "/" + tokens[1] groupVersion, err := schema.ParseGroupVersion(groupVersionString) if err != nil { return nil, fmt.Errorf("runtime-config invalid key %s", key) } - groups = append(groups, groupVersion.Group) + groupVersions = append(groupVersions, groupVersion) } - return groups, nil + return groupVersions, nil } // EmulationForwardCompatibleResourceConfig creates a new ResourceConfig that besides all the enabled resources in resourceConfig, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/helpers_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/helpers_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/resourceconfig/helpers_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/resourceconfig/helpers_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/debugsocket.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/debugsocket.go similarity index 85% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/debugsocket.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/debugsocket.go index e7297b35f..4deed5a5c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/debugsocket.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/debugsocket.go @@ -17,12 +17,15 @@ limitations under the License. package routes import ( + "context" "fmt" "net" "net/http" "net/http/pprof" "os" "path" + + "k8s.io/apimachinery/pkg/util/wait" ) // DebugSocket installs profiling and debugflag as a Unix-Domain socket. @@ -62,7 +65,14 @@ func (s *DebugSocket) InstallDebugFlag(flag string, handler func(http.ResponseWr } // Run starts the server and waits for stopCh to be closed to close the server. +// +//logcheck:context // RunWithContext should be used instead of Run in code which supports contextual logging. func (s *DebugSocket) Run(stopCh <-chan struct{}) error { + return s.RunWithContext(wait.ContextForChannel(stopCh)) +} + +// RunWithContext starts the server and waits for the context to be cancelled to close the server. +func (s *DebugSocket) RunWithContext(ctx context.Context) error { if err := os.Remove(s.path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove (%v): %v", s.path, err) } @@ -75,7 +85,7 @@ func (s *DebugSocket) Run(stopCh <-chan struct{}) error { srv := http.Server{Handler: s.mux} go func() { - <-stopCh + <-ctx.Done() srv.Close() }() return srv.Serve(l) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/flags.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/flags.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/flags.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/flags.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/index.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/index.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/index.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/index.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/metrics.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/metrics.go index 8fd4d5599..644020049 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/metrics.go @@ -24,6 +24,7 @@ import ( etcd3metrics "k8s.io/apiserver/pkg/storage/etcd3/metrics" flowcontrolmetrics "k8s.io/apiserver/pkg/util/flowcontrol/metrics" peerproxymetrics "k8s.io/apiserver/pkg/util/peerproxy/metrics" + proxymetrics "k8s.io/apiserver/pkg/util/proxy/metrics" "k8s.io/component-base/metrics/legacyregistry" ) @@ -54,4 +55,5 @@ func register() { flowcontrolmetrics.Register() peerproxymetrics.Register() handlersmetrics.Register() + proxymetrics.Register() } diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi.go new file mode 100644 index 000000000..ee830a267 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi.go @@ -0,0 +1,184 @@ +/* +Copyright 2016 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 routes + +import ( + "strings" + + restful "github.com/emicklei/go-restful/v3" + "k8s.io/klog/v2" + + "k8s.io/apiserver/pkg/server/mux" + builder2 "k8s.io/kube-openapi/pkg/builder" + "k8s.io/kube-openapi/pkg/builder3" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common/restfuladapter" + "k8s.io/kube-openapi/pkg/handler" + "k8s.io/kube-openapi/pkg/handler3" + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +// OpenAPI installs spec endpoints for each web service. +type OpenAPI struct { + Config *common.Config + V3Config *common.OpenAPIV3Config +} + +// Install adds the SwaggerUI webservice to the given mux. +func (oa OpenAPI) InstallV2(c *restful.Container, mux *mux.PathRecorderMux) (*handler.OpenAPIService, *spec.Swagger) { + spec, err := builder2.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(c.RegisteredWebServices()), oa.Config) + if err != nil { + klog.Fatalf("Failed to build open api spec for root: %v", err) + } + spec.Definitions = handler.PruneDefaults(spec.Definitions) + openAPIVersionedService := handler.NewOpenAPIService(spec) + openAPIVersionedService.RegisterOpenAPIVersionedService("/openapi/v2", mux) + + return openAPIVersionedService, spec +} + +// InstallV3 adds the static group/versions defined in the RegisteredWebServices to the OpenAPI v3 spec. +// This only covers built-in resources served via go-restful; CRDs and aggregated APIs publish +// their OpenAPI v3 specs through separate code paths. +func (oa OpenAPI) InstallV3(c *restful.Container, mux *mux.PathRecorderMux) *handler3.OpenAPIService { + openAPIVersionedService := handler3.NewOpenAPIService() + err := openAPIVersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", mux) + if err != nil { + klog.Fatalf("Failed to register versioned open api spec for root: %v", err) + } + + grouped := make(map[string][]*restful.WebService) + + for _, t := range c.RegisteredWebServices() { + // Strip the "/" prefix from the name + gvName := t.RootPath()[1:] + grouped[gvName] = []*restful.WebService{t} + } + + for gv, ws := range grouped { + spec, err := builder3.BuildOpenAPISpecFromRoutes(restfuladapter.AdaptWebServices(ws), oa.V3Config) + if err != nil { + klog.Errorf("Failed to build OpenAPI v3 for group %s, %q", gv, err) + continue + } + if group, version, ok := groupVersionFromPath(gv); ok { + filterScopedGVKs(spec, group, version) + } + openAPIVersionedService.UpdateGroupVersion(gv, spec) + } + return openAPIVersionedService +} + +// groupVersionFromPath extracts the API group and version from a root path like "apis/apps/v1" or "api/v1". +func groupVersionFromPath(path string) (group, version string, ok bool) { + // "api/v1" → ("", "v1", true) + // "apis/apps/v1" → ("apps", "v1", true) + // "apis/networking.k8s.io/v1" → ("networking.k8s.io", "v1", true) + parts := strings.SplitN(path, "/", 4) + switch { + case len(parts) < 2: + return "", "", false + case parts[0] == "api" && len(parts) == 2: + return "", parts[1], true + case parts[0] == "apis" && len(parts) == 3: + return parts[1], parts[2], true + default: + return "", "", false + } +} + +// crossRegisteredKinds lists the kinds that AddToGroupVersion (in +// k8s.io/apimachinery/pkg/apis/meta/v1) registers into every API group. +// Only these types get their x-kubernetes-group-version-kind list filtered +// in per-GV v3 specs. +var crossRegisteredKinds = map[string]bool{ + "WatchEvent": true, + "DeleteOptions": true, +} + +// filterScopedGVKs narrows x-kubernetes-group-version-kind on the meta types +// that AddToGroupVersion cross-registers into every API group (WatchEvent and +// DeleteOptions). Without filtering, each per-GV v3 spec carries ~60 GVKs for +// these types. This filter keeps only the entry matching this spec's group/version +// plus the canonical core/v1 entry. +// +// Only the meta types listed in crossRegisteredKinds are filtered. Other types +// that are intentionally registered into specific groups (like autoscaling/v1 +// Scale into apps/v1 and core/v1) are left unchanged. +// +// This only affects built-in resource specs generated by InstallV3 above. +// CRDs and aggregated APIs are not affected as they publish specs through separate paths. +func filterScopedGVKs(s *spec3.OpenAPI, group, version string) { + if s == nil || s.Components == nil { + return + } + for _, schema := range s.Components.Schemas { + if schema == nil { + continue + } + ext, ok := schema.Extensions["x-kubernetes-group-version-kind"] + if !ok { + continue + } + gvks, ok := ext.([]interface{}) + if !ok || len(gvks) <= 1 { + continue + } + if !isCrossRegisteredKind(gvks) { + continue + } + var filtered []interface{} + for _, item := range gvks { + m, ok := item.(map[string]interface{}) + if !ok { + continue + } + g, ok := m["group"].(string) + if !ok { + continue + } + v, ok := m["version"].(string) + if !ok { + continue + } + if (g == group && v == version) || (g == "" && v == "v1") { + filtered = append(filtered, item) + } + } + if len(filtered) > 0 { + schema.Extensions["x-kubernetes-group-version-kind"] = filtered + } else { + klog.Warningf("Unexpected: filtering x-kubernetes-group-version-kind for %s/%s produced no matches", group, version) + delete(schema.Extensions, "x-kubernetes-group-version-kind") + } + } +} + +// isCrossRegisteredKind checks whether a GVK list represents one of the +// meta types from crossRegisteredKinds by inspecting the first entry's kind. +func isCrossRegisteredKind(gvks []interface{}) bool { + if len(gvks) == 0 { + return false + } + m, ok := gvks[0].(map[string]interface{}) + if !ok { + return false + } + kind, _ := m["kind"].(string) + return crossRegisteredKinds[kind] +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi_test.go new file mode 100644 index 000000000..6f0f93b29 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/openapi_test.go @@ -0,0 +1,177 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package routes + +import ( + "reflect" + "testing" + + "k8s.io/kube-openapi/pkg/spec3" + "k8s.io/kube-openapi/pkg/validation/spec" +) + +func TestGroupVersionFromPath(t *testing.T) { + tests := []struct { + path string + wantGroup string + wantVersion string + wantOK bool + }{ + {"api/v1", "", "v1", true}, + {"apis/apps/v1", "apps", "v1", true}, + {"apis/networking.k8s.io/v1", "networking.k8s.io", "v1", true}, + {"apis/batch/v1", "batch", "v1", true}, + {"api", "", "", false}, + {"apis/apps", "", "", false}, + {"api/v1/extra", "", "", false}, + {"apis/apps/v1/extra", "", "", false}, + {"", "", "", false}, + } + for _, tt := range tests { + group, version, ok := groupVersionFromPath(tt.path) + if group != tt.wantGroup || version != tt.wantVersion || ok != tt.wantOK { + t.Errorf("groupVersionFromPath(%q) = (%q, %q, %v), want (%q, %q, %v)", tt.path, group, version, ok, tt.wantGroup, tt.wantVersion, tt.wantOK) + } + } +} + +func gvk(group, version, kind string) map[string]interface{} { + return map[string]interface{}{"group": group, "version": version, "kind": kind} +} + +func TestFilterScopedGVKs(t *testing.T) { + tests := []struct { + name string + gvks []interface{} + group string + version string + wantGVKs []interface{} + }{ + { + name: "DeleteOptions keeps local and core/v1", + gvks: []interface{}{ + gvk("", "v1", "DeleteOptions"), + gvk("apps", "v1", "DeleteOptions"), + gvk("batch", "v1", "DeleteOptions"), + gvk("autoscaling", "v1", "DeleteOptions"), + }, + group: "apps", + version: "v1", + wantGVKs: []interface{}{ + gvk("", "v1", "DeleteOptions"), + gvk("apps", "v1", "DeleteOptions"), + }, + }, + { + name: "WatchEvent keeps local and core/v1", + gvks: []interface{}{ + gvk("", "v1", "WatchEvent"), + gvk("apps", "v1", "WatchEvent"), + gvk("batch", "v1", "WatchEvent"), + }, + group: "batch", + version: "v1", + wantGVKs: []interface{}{ + gvk("", "v1", "WatchEvent"), + gvk("batch", "v1", "WatchEvent"), + }, + }, + { + name: "DeleteOptions with core group keeps only core/v1", + gvks: []interface{}{ + gvk("", "v1", "DeleteOptions"), + gvk("apps", "v1", "DeleteOptions"), + gvk("batch", "v1", "DeleteOptions"), + }, + group: "", + version: "v1", + wantGVKs: []interface{}{gvk("", "v1", "DeleteOptions")}, + }, + { + name: "single GVK unchanged", + gvks: []interface{}{ + gvk("apps", "v1", "Deployment"), + }, + group: "apps", + version: "v1", + wantGVKs: []interface{}{gvk("apps", "v1", "Deployment")}, + }, + { + name: "non-meta cross-group type unchanged", + gvks: []interface{}{ + gvk("autoscaling", "v1", "Scale"), + gvk("apps", "v1", "Scale"), + }, + group: "apps", + version: "v1", + wantGVKs: []interface{}{ + gvk("autoscaling", "v1", "Scale"), + gvk("apps", "v1", "Scale"), + }, + }, + { + name: "DeleteOptions no match removes extension", + gvks: []interface{}{ + gvk("apps", "v1", "DeleteOptions"), + gvk("batch", "v1", "DeleteOptions"), + }, + group: "networking.k8s.io", + version: "v1", + wantGVKs: nil, + }, + { + name: "multi-version same group unchanged", + gvks: []interface{}{ + gvk("autoscaling", "v1", "HorizontalPodAutoscaler"), + gvk("autoscaling", "v2", "HorizontalPodAutoscaler"), + }, + group: "autoscaling", + version: "v2", + wantGVKs: []interface{}{ + gvk("autoscaling", "v1", "HorizontalPodAutoscaler"), + gvk("autoscaling", "v2", "HorizontalPodAutoscaler"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &spec3.OpenAPI{ + Components: &spec3.Components{ + Schemas: map[string]*spec.Schema{ + "TestType": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-group-version-kind": tt.gvks, + }, + }, + }, + }, + }, + } + filterScopedGVKs(s, tt.group, tt.version) + got, exists := s.Components.Schemas["TestType"].Extensions["x-kubernetes-group-version-kind"] + if tt.wantGVKs == nil { + if exists { + t.Errorf("expected extension to be removed, got %v", got) + } + } else if !reflect.DeepEqual(got, tt.wantGVKs) { + t.Errorf("got %v, want %v", got, tt.wantGVKs) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/profiling.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/profiling.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/profiling.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/profiling.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/version.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/version.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routes/version.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routes/version.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routine/routine.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routine/routine.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routine/routine.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routine/routine.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/routine/routine_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/routine/routine_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/routine/routine_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/routine/routine_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/secure_serving.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/secure_serving.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/secure_serving.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/secure_serving.go index 867c7efac..0c14893a7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/secure_serving.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/secure_serving.go @@ -71,6 +71,9 @@ func (s *SecureServingInfo) tlsConfig(stopCh <-chan struct{}) (*tls.Config, erro } } } + if len(s.CurvePreferences) > 0 { + tlsConfig.CurvePreferences = s.CurvePreferences + } if s.ClientCA != nil { // Populate PeerCertificates in requests, but don't reject connections without certificates diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal_posix.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal_posix.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal_posix.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal_posix.go index 7acb2038a..fc0ab2d67 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal_posix.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal_posix.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2017 The Kubernetes Authors. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal_windows.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal_windows.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/signal_windows.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/signal_windows.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/doc.go new file mode 100644 index 000000000..9a542b097 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/doc.go @@ -0,0 +1,22 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.statusz.api.v1alpha1 + +// Package v1alpha1 contains API Schema definitions for the statusz v1alpha1 API group +package v1alpha1 diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/register.go new file mode 100644 index 000000000..6a64dc21b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/register.go @@ -0,0 +1,47 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +const ( + GroupName = "config.k8s.io" + Version = "v1alpha1" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + +var ( + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that adds this group's types to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Statusz{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/types.go new file mode 100644 index 000000000..ccbe05cb8 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/types.go @@ -0,0 +1,50 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// Statusz is a struct used for versioned statusz endpoint. +type Statusz struct { + // TypeMeta is the type metadata for the object. + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // StartTime is the time the component process was initiated. + StartTime metav1.Time `json:"startTime"` + // UptimeSeconds is the duration in seconds for which the component has been running continuously. + UptimeSeconds int64 `json:"uptimeSeconds"` + // GoVersion is the version of the Go programming language used to build the binary. + // The format is not guaranteed to be consistent across different Go builds. + // +optional + GoVersion string `json:"goVersion"` + // BinaryVersion is the version of the component's binary. + // The format is not guaranteed to be semantic versioning and may be an arbitrary string. + BinaryVersion string `json:"binaryVersion"` + // EmulationVersion is the Kubernetes API version which this component is emulating. + // if present, formatted as "." + // +optional + EmulationVersion string `json:"emulationVersion,omitempty"` + // Paths contains relative URLs to other essential read-only endpoints for debugging and troubleshooting. + // +optional + // +listType=set + Paths []string `json:"paths"` +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 000000000..e36206ae4 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,58 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Statusz) DeepCopyInto(out *Statusz) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.StartTime.DeepCopyInto(&out.StartTime) + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Statusz. +func (in *Statusz) DeepCopy() *Statusz { + if in == nil { + return nil + } + out := new(Statusz) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Statusz) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.model_name.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.model_name.go new file mode 100644 index 000000000..8f2bca4ce --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1alpha1/zz_generated.model_name.go @@ -0,0 +1,27 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1alpha1 + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Statusz) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.server.statusz.api.v1alpha1.Statusz" +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/doc.go new file mode 100644 index 000000000..8237e4776 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/doc.go @@ -0,0 +1,22 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:deepcopy-gen=package +// +k8s:openapi-gen=true +// +k8s:openapi-model-package=io.k8s.apiserver.pkg.server.statusz.api.v1beta1 + +// Package v1beta1 contains API Schema definitions for the statusz v1beta1 API group +package v1beta1 diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/register.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/register.go new file mode 100644 index 000000000..c41806b69 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/register.go @@ -0,0 +1,47 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +const ( + GroupName = "config.k8s.io" + Version = "v1beta1" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} + +var ( + // SchemeBuilder initializes a scheme builder + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a global function that adds this group's types to a scheme + AddToScheme = SchemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Statusz{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/types.go new file mode 100644 index 000000000..a8898826b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/types.go @@ -0,0 +1,50 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// Statusz is a struct used for versioned statusz endpoint. +type Statusz struct { + // TypeMeta is the type metadata for the object. + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // StartTime is the time the component process was initiated. + StartTime metav1.Time `json:"startTime"` + // UptimeSeconds is the duration in seconds for which the component has been running continuously. + UptimeSeconds int64 `json:"uptimeSeconds"` + // GoVersion is the version of the Go programming language used to build the binary. + // The format is not guaranteed to be consistent across different Go builds. + // +optional + GoVersion string `json:"goVersion"` + // BinaryVersion is the version of the component's binary. + // The format is not guaranteed to be semantic versioning and may be an arbitrary string. + BinaryVersion string `json:"binaryVersion"` + // EmulationVersion is the Kubernetes API version which this component is emulating. + // if present, formatted as "." + // +optional + EmulationVersion string `json:"emulationVersion,omitempty"` + // Paths contains relative URLs to other essential read-only endpoints for debugging and troubleshooting. + // +optional + // +listType=set + Paths []string `json:"paths"` +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..e879af624 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,58 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Statusz) DeepCopyInto(out *Statusz) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.StartTime.DeepCopyInto(&out.StartTime) + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Statusz. +func (in *Statusz) DeepCopy() *Statusz { + if in == nil { + return nil + } + out := new(Statusz) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Statusz) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.model_name.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.model_name.go new file mode 100644 index 000000000..8a4134750 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/api/v1beta1/zz_generated.model_name.go @@ -0,0 +1,27 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by openapi-gen. DO NOT EDIT. + +package v1beta1 + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in Statusz) OpenAPIModelName() string { + return "io.k8s.apiserver.pkg.server.statusz.api.v1beta1.Statusz" +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/negotiate/negotiation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/negotiate/negotiation.go new file mode 100644 index 000000000..cd314e4ae --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/negotiate/negotiation.go @@ -0,0 +1,48 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package negotiate + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// StatuszEndpointRestrictions implements content negotiation restrictions for statusz. +// It is used to validate and restrict which GroupVersionKinds are allowed for structured responses. +type StatuszEndpointRestrictions struct { + RecognizedStructuredKinds map[schema.GroupVersionKind]bool +} + +// AllowsMediaTypeTransform checks if the provided GVK is supported for structured statusz responses. +func (s StatuszEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool { + if mimeType == "text" && mimeSubType == "plain" { + return gvk == nil + } + + if gvk != nil { + return s.RecognizedStructuredKinds[*gvk] + } + + return false +} + +func (StatuszEndpointRestrictions) AllowsServerVersion(s string) bool { + return false +} + +func (StatuszEndpointRestrictions) AllowsStreamSchema(s string) bool { + return false +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry.go new file mode 100644 index 000000000..d27cb9400 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry.go @@ -0,0 +1,97 @@ +/* +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 statusz + +import ( + "time" + + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/component-base/compatibility" + "k8s.io/klog/v2" + + compbasemetrics "k8s.io/component-base/metrics" + utilversion "k8s.io/component-base/version" +) + +type statuszRegistry interface { + processStartTime() time.Time + goVersion() string + binaryVersion() *version.Version + emulationVersion() *version.Version + paths() []string + deprecatedVersions() map[string]bool +} + +type registry struct { + // componentGlobalsRegistry compatibility.ComponentGlobalsRegistry + effectiveVersion compatibility.EffectiveVersion + // listedPaths is an alphabetically sorted list of paths to be reported at /. + listedPaths []string + // deprecatedVersionsMap is a map of deprecated statusz versions. + deprecatedVersionsMap map[string]bool +} + +// Option is a function to configure registry. +type Option func(reg *registry) + +// WithListedPaths returns an Option to configure the ListedPaths. +func WithListedPaths(listedPaths []string) Option { + cpyListedPaths := make([]string, len(listedPaths)) + copy(cpyListedPaths, listedPaths) + + return func(reg *registry) { reg.listedPaths = cpyListedPaths } +} + +func (*registry) processStartTime() time.Time { + start, err := compbasemetrics.GetProcessStart() + if err != nil { + klog.Errorf("Could not get process start time, %v", err) + } + + return time.Unix(int64(start), 0) +} + +func (*registry) goVersion() string { + return utilversion.Get().GoVersion +} + +func (r *registry) binaryVersion() *version.Version { + if r.effectiveVersion != nil { + return r.effectiveVersion.BinaryVersion() + } + return version.MustParse(utilversion.Get().String()) +} + +func (r *registry) emulationVersion() *version.Version { + if r.effectiveVersion != nil { + return r.effectiveVersion.EmulationVersion() + } + + return nil +} + +func (r *registry) paths() []string { + if r.listedPaths != nil { + return r.listedPaths + } + + return nil +} + +func (r *registry) deprecatedVersions() map[string]bool { + return r.deprecatedVersionsMap +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry_test.go new file mode 100644 index 000000000..3d866953f --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/registry_test.go @@ -0,0 +1,98 @@ +/* +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 statusz + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/component-base/compatibility" + utilversion "k8s.io/component-base/version" +) + +func TestBinaryVersion(t *testing.T) { + tests := []struct { + name string + setFakeEffectiveVersion bool + fakeVersion string + wantBinaryVersion *version.Version + }{ + { + name: "binaryVersion with effective version", + wantBinaryVersion: version.MustParseSemantic("v1.2.3"), + setFakeEffectiveVersion: true, + fakeVersion: "1.2.3", + }, + { + name: "binaryVersion without effective version", + wantBinaryVersion: version.MustParse(utilversion.Get().String()), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + registry := ®istry{} + if tt.setFakeEffectiveVersion { + verKube := compatibility.NewEffectiveVersionFromString(tt.fakeVersion, "", "") + registry.effectiveVersion = verKube + } + + got := registry.binaryVersion() + assert.Equal(t, tt.wantBinaryVersion, got) + }) + } +} + +func TestEmulationVersion(t *testing.T) { + tests := []struct { + name string + setFakeEffectiveVersion bool + fakeEmulVer string + wantEmul *version.Version + }{ + { + name: "emulationVersion with effective version", + fakeEmulVer: "2.3.4", + setFakeEffectiveVersion: true, + wantEmul: version.MustParseSemantic("2.3.4"), + }, + { + name: "emulationVersion without effective version", + wantEmul: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + registry := ®istry{} + if tt.setFakeEffectiveVersion { + verKube := compatibility.NewEffectiveVersionFromString("0.0.0", "", "") + verKube.SetEmulationVersion(version.MustParse(tt.fakeEmulVer)) + registry.effectiveVersion = verKube + } + + got := registry.emulationVersion() + if tt.wantEmul != nil && got != nil { + assert.Equal(t, tt.wantEmul.Major(), got.Major()) + assert.Equal(t, tt.wantEmul.Minor(), got.Minor()) + } else { + assert.Equal(t, tt.wantEmul, got) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz.go new file mode 100644 index 000000000..1b334b169 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz.go @@ -0,0 +1,388 @@ +/* +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 statusz + +import ( + "fmt" + "net/http" + "sort" + "strings" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor" + "k8s.io/component-base/compatibility" + + "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/endpoints/responsewriter" + "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" + "k8s.io/apiserver/pkg/server/statusz/api/v1beta1" + "k8s.io/apiserver/pkg/server/statusz/negotiate" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + utilfeature "k8s.io/apiserver/pkg/util/feature" +) + +var ( + delimiters = []string{":", ": ", "=", " "} + nonDebuggingEndpoints = map[string]bool{ + "/apis": true, + "/api": true, + "/openid": true, + "/openapi": true, + "/.well-known": true, + } + v1alpha1StatuszKind = v1alpha1.SchemeGroupVersion.WithKind("Statusz") + v1beta1StatuszKind = v1beta1.SchemeGroupVersion.WithKind("Statusz") + recognizedStructuredKinds = map[schema.GroupVersionKind]bool{ + v1alpha1StatuszKind: true, + v1beta1StatuszKind: true, + } +) + +const DefaultStatuszPath = "/statusz" + +const headerFmt = ` +%s statusz +Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. +` + +// statuszCodecFactory wraps a CodecFactory to filter out unsupported media types (like protobuf) +// from the supported media types list, so error messages only show actually supported types. +type statuszCodecFactory struct { + serializer.CodecFactory + supportedMediaTypes []runtime.SerializerInfo +} + +type mux interface { + Handle(path string, handler http.Handler) +} + +type ListedPathsOption []string + +func NewRegistry(effectiveVersion compatibility.EffectiveVersion, opts ...Option) statuszRegistry { + r := ®istry{ + effectiveVersion: effectiveVersion, + deprecatedVersionsMap: map[string]bool{"v1alpha1": true}, + } + for _, opt := range opts { + opt(r) + } + + return r +} + +func Install(m mux, componentName string, reg statuszRegistry) { + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) + filteredCodecFactory, err := newStatuszCodecFactory(scheme, componentName, reg) + if err != nil { + utilruntime.HandleError(err) + } + restrictions := negotiate.StatuszEndpointRestrictions{ + RecognizedStructuredKinds: recognizedStructuredKinds, + } + m.Handle(DefaultStatuszPath, handleStatusz(componentName, reg, filteredCodecFactory, restrictions)) +} + +// newStatuszCodecFactory creates a codec factory with the standard serializers for statusz, +// filtering out unsupported media types (e.g., protobuf). +func newStatuszCodecFactory(scheme *runtime.Scheme, componentName string, reg statuszRegistry) (*statuszCodecFactory, error) { + codecFactoryOpts := []serializer.CodecFactoryOptionsMutator{ + serializer.WithSerializer(func(_ runtime.ObjectCreater, _ runtime.ObjectTyper) runtime.SerializerInfo { + textSerializer := statuszTextSerializer{componentName, reg} + return runtime.SerializerInfo{ + MediaType: "text/plain", + MediaTypeType: "text", + MediaTypeSubType: "plain", + EncodesAsText: true, + Serializer: textSerializer, + PrettySerializer: textSerializer, + } + }), + } + // TODO: remove this explicit check when https://github.com/kubernetes/enhancements/pull/5740 is implemented. + if utilfeature.DefaultFeatureGate.Enabled(features.CBORServingAndStorage) { + codecFactoryOpts = append(codecFactoryOpts, serializer.WithSerializer(cbor.NewSerializerInfo)) + } + + codecFactory := serializer.NewCodecFactory(scheme, codecFactoryOpts...) + allTypes := codecFactory.SupportedMediaTypes() + filtered := make([]runtime.SerializerInfo, 0, len(allTypes)) + + var unknownTypes []string + for _, info := range allTypes { + switch info.MediaType { + // Supported media types + case "text/plain", runtime.ContentTypeJSON, runtime.ContentTypeYAML, runtime.ContentTypeCBOR: + filtered = append(filtered, info) + // Unsupported media types + case runtime.ContentTypeProtobuf: + continue + default: + unknownTypes = append(unknownTypes, info.MediaType) + } + } + + var err error + if len(unknownTypes) > 0 { + err = fmt.Errorf("statusz: unknown media type(s) %v, excluding from supported types", unknownTypes) + } + + return &statuszCodecFactory{ + CodecFactory: codecFactory, + supportedMediaTypes: filtered, + }, err +} + +func (f *statuszCodecFactory) SupportedMediaTypes() []runtime.SerializerInfo { + return f.supportedMediaTypes +} + +func handleStatusz(componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(r.Context()) + if !ok { + requestReceivedTimestamp = time.Now() + } + delegate := &metrics.ResponseWriterDelegator{ResponseWriter: w} + w = responsewriter.WrapForHTTP1Or2(delegate) + + // Use MonitorRequest instead of InstrumentHandlerFunc because the group, + // version, and deprecated status depend on per-request content negotiation. + // For text/plain requests, group and version remain empty. For structured + // responses (JSON/YAML/CBOR), they are set to the negotiated API group and + // version (e.g., config.k8s.io/v1alpha1). + var group, version string + var deprecated bool + defer func() { + metrics.MonitorRequest(r, "GET", group, version, + "statusz", // resource + "", // subresource + "", // scope + componentName, // component + deprecated, + "", // removedRelease + delegate.Status(), delegate.ContentLength(), time.Since(requestReceivedTimestamp)) + }() + + acceptHeader := r.Header.Get("Accept") + if strings.TrimSpace(acceptHeader) == "" { + writePlainTextResponse(v1beta1Statusz(componentName, reg), serializer, w) + return + } + + mediaType, serializerInfo, err := negotiation.NegotiateOutputMediaType(r, serializer, restrictions) + if err != nil { + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + return + } + + switch serializerInfo.MediaType { + case "application/json", "application/yaml", "application/cbor": + if mediaType.Convert == nil { + err := fmt.Errorf("content negotiation failed: mediaType.Convert is nil for %s", serializerInfo.MediaType) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + return + } + // Set group, version, and deprecated from the negotiated target so + // the deferred MonitorRequest records the actual requested API version. + group = mediaType.Convert.Group + version = mediaType.Convert.Version + deprecated = reg.deprecatedVersions()[version] + if deprecated { + w.Header().Set("Warning", `299 - "This version of the statusz endpoint is deprecated. Please use a newer version."`) + } + handleStructuredResponse(w, r, componentName, reg, serializer, restrictions, mediaType) + case "text/plain": + writePlainTextResponse(v1beta1Statusz(componentName, reg), serializer, w) + default: + err := fmt.Errorf("unsupported media type: %s/%s", serializerInfo.MediaType, serializerInfo.MediaTypeSubType) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + } + } +} + +func handleStructuredResponse(w http.ResponseWriter, r *http.Request, componentName string, reg statuszRegistry, serializer runtime.NegotiatedSerializer, restrictions negotiate.StatuszEndpointRestrictions, mediaType negotiation.MediaTypeOptions) { + switch *mediaType.Convert { + case v1alpha1StatuszKind: + writeStructuredResponse(v1alpha1Statusz(componentName, reg), serializer, mediaType.Convert.GroupVersion(), restrictions, w, r) + case v1beta1StatuszKind: + writeStructuredResponse(v1beta1Statusz(componentName, reg), serializer, mediaType.Convert.GroupVersion(), restrictions, w, r) + default: + err := fmt.Errorf("unsupported media type: %s", mediaType.Convert.String()) + utilruntime.HandleError(err) + responsewriters.ErrorNegotiated( + err, + serializer, + schema.GroupVersion{}, + w, + r, + ) + } +} + +func writePlainTextResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + // Find the text/plain serializer + var textSerializer runtime.Serializer + for _, info := range serializer.SupportedMediaTypes() { + if info.MediaType == "text/plain" { + textSerializer = info.Serializer + break + } + } + if textSerializer == nil { + utilruntime.HandleError(fmt.Errorf("text/plain serializer not available")) + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := textSerializer.Encode(obj, w); err != nil { + utilruntime.HandleError(fmt.Errorf("error encoding statusz as text/plain: %w", err)) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func writeStructuredResponse(obj runtime.Object, serializer runtime.NegotiatedSerializer, targetGV schema.GroupVersion, restrictions negotiate.StatuszEndpointRestrictions, w http.ResponseWriter, r *http.Request) { + responsewriters.WriteObjectNegotiated( + serializer, + restrictions, + targetGV, + w, + r, + http.StatusOK, + obj, + true, + ) +} + +func v1alpha1Statusz(componentName string, reg statuszRegistry) *v1alpha1.Statusz { + startTime := reg.processStartTime() + upTimeSeconds := max(0, int64(time.Since(startTime).Seconds())) + goVersion := reg.goVersion() + binaryVersion := reg.binaryVersion().String() + var emulationVersion string + if reg.emulationVersion() != nil { + emulationVersion = reg.emulationVersion().String() + } + + paths := aggregatePaths(reg.paths()) + data := &v1alpha1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1StatuszKind.Kind, + APIVersion: v1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: componentName, + }, + StartTime: metav1.Time{Time: startTime}, + UptimeSeconds: upTimeSeconds, + GoVersion: goVersion, + BinaryVersion: binaryVersion, + EmulationVersion: emulationVersion, + Paths: paths, + } + + return data +} + +func v1beta1Statusz(componentName string, reg statuszRegistry) *v1beta1.Statusz { + startTime := reg.processStartTime() + upTimeSeconds := max(0, int64(time.Since(startTime).Seconds())) + goVersion := reg.goVersion() + binaryVersion := reg.binaryVersion().String() + var emulationVersion string + if reg.emulationVersion() != nil { + emulationVersion = reg.emulationVersion().String() + } + + paths := aggregatePaths(reg.paths()) + data := &v1beta1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: v1beta1StatuszKind.Kind, + APIVersion: v1beta1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: componentName, + }, + StartTime: metav1.Time{Time: startTime}, + UptimeSeconds: upTimeSeconds, + GoVersion: goVersion, + BinaryVersion: binaryVersion, + EmulationVersion: emulationVersion, + Paths: paths, + } + + return data +} + +func uptime(t time.Time) string { + upSince := int64(time.Since(t).Seconds()) + return fmt.Sprintf("%d hr %02d min %02d sec", + upSince/3600, (upSince/60)%60, upSince%60) +} + +func aggregatePaths(listedPaths []string) []string { + paths := make(map[string]bool) + for _, listedPath := range listedPaths { + parts := strings.Split(listedPath, "/") + if len(parts) < 2 || parts[1] == "" { + continue + } + folder := "/" + parts[1] + if !paths[folder] && !nonDebuggingEndpoints[folder] { + paths[folder] = true + } + } + + var sortedPaths []string + for p := range paths { + sortedPaths = append(sortedPaths, p) + } + sort.Strings(sortedPaths) + + return sortedPaths +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz_test.go new file mode 100644 index 000000000..ff81ee62c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/statusz_test.go @@ -0,0 +1,518 @@ +/* +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 statusz + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "k8s.io/apimachinery/pkg/runtime" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1alpha1 "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" + v1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1" +) + +const wantTmpl = ` +%s statusz +Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. + +Started: %v +Up: %s +Go version: %s +Binary version: %v +Emulation version: %v +Paths: /livez /readyz +` + +const wantTmplWithoutEmulation = ` +%s statusz +Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. + +Started: %v +Up: %s +Go version: %s +Binary version: %v + +Paths: /livez /readyz +` + +func TestHandleStatusz(t *testing.T) { + // Enable CBOR feature gate for CBOR test case + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + + delimiters = []string{":"} + fakeStartTime := time.Now() + fakeUptime := uptime(fakeStartTime) + fakeGoVersion := "1.21" + fakeBvStr := "1.31" + fakeEvStr := "1.30" + fakeBinaryVersion := parseVersion(t, fakeBvStr) + fakeEmulationVersion := parseVersion(t, fakeEvStr) + fakeListedPaths := []string{"/livez/poststarthook/peer-discovery-cache-sync", "/livez/post", "/readyz/informer-sync", "/readyz/log", "/readyz/ping"} + tests := []struct { + name string + acceptHeader string + componentName string + registry fakeRegistry + wantStatusCode int + wantBody string + wantStructuredBody interface{} + wantWarning bool + }{ + { + name: "valid request for text/plain", + acceptHeader: "text/plain", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + fakeStartTime.Format(time.UnixDate), + fakeUptime, + fakeGoVersion, + fakeBinaryVersion, + fakeEmulationVersion, + ), + }, + { + name: "valid request for application/json", + acceptHeader: "application/json;v=v1beta1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Statusz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + }, + { + name: "no accept header", + acceptHeader: "", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + fakeStartTime.Format(time.UnixDate), + fakeUptime, + fakeGoVersion, + fakeBinaryVersion, + fakeEmulationVersion, + ), + }, + { + name: "invalid accept header", + acceptHeader: "application/xml", + componentName: "test-server", + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "missing emulation version", + acceptHeader: "text/plain", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: nil, + listedPaths: fakeListedPaths, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmplWithoutEmulation, + "test-server", + fakeStartTime.Format(time.UnixDate), + fakeUptime, + fakeGoVersion, + fakeBinaryVersion, + ), + }, + { + name: "application/json without params", + acceptHeader: "application/json", + componentName: "test-server", + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "application/json with missing as", + acceptHeader: "application/json;v=v1beta1;g=config.k8s.io", + componentName: "test-server", + wantStatusCode: http.StatusNotAcceptable, + }, + { + name: "wildcard accept header", + acceptHeader: "*/*", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + fakeStartTime.Format(time.UnixDate), + fakeUptime, + fakeGoVersion, + fakeBinaryVersion, + fakeEmulationVersion, + ), + }, + { + name: "bad json header fall back wildcard", + acceptHeader: "application/json;v=foo;g=config.k8s.io;as=Statusz,*/*", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + }, + wantStatusCode: http.StatusOK, + wantBody: fmt.Sprintf( + wantTmpl, + "test-server", + fakeStartTime.Format(time.UnixDate), + fakeUptime, + fakeGoVersion, + fakeBinaryVersion, + fakeEmulationVersion, + ), + }, + { + name: "deprecated v1alpha1 request", + acceptHeader: "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{"v1alpha1": true}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1alpha1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Statusz", + APIVersion: "config.k8s.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + wantWarning: true, + }, + { + name: "valid request for application/yaml", + acceptHeader: "application/yaml;v=v1beta1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Statusz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + }, + { + name: "valid request for application/cbor", + acceptHeader: "application/cbor;v=v1beta1;g=config.k8s.io;as=Statusz", + componentName: "test-server", + registry: fakeRegistry{ + startTime: fakeStartTime, + goVer: fakeGoVersion, + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + deprecated: map[string]bool{}, + }, + wantStatusCode: http.StatusOK, + wantStructuredBody: &v1beta1.Statusz{ + TypeMeta: metav1.TypeMeta{ + Kind: "Statusz", + APIVersion: "config.k8s.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-server", + }, + StartTime: metav1.Time{Time: fakeStartTime}, + UptimeSeconds: int64(time.Since(fakeStartTime).Seconds()), + GoVersion: fakeGoVersion, + BinaryVersion: fakeBvStr, + EmulationVersion: fakeEvStr, + Paths: []string{"/livez", "/readyz"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mux := http.NewServeMux() + Install(mux, tt.componentName, tt.registry) + + path := "/statusz" + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil) + if err != nil { + t.Fatalf("unexpected error while creating request: %v", err) + } + if tt.acceptHeader != "" { + req.Header.Set("Accept", tt.acceptHeader) + } + + w := httptest.NewRecorder() + mux.ServeHTTP(w, req) + + if w.Code != tt.wantStatusCode { + t.Fatalf("want status code: %v, got: %v", tt.wantStatusCode, w.Code) + } + + if tt.wantStatusCode == http.StatusOK { + if tt.wantStructuredBody != nil { + var got interface{} + switch tt.wantStructuredBody.(type) { + case *v1alpha1.Statusz: + got = &v1alpha1.Statusz{} + case *v1beta1.Statusz: + got = &v1beta1.Statusz{} + default: + t.Fatalf("unexpected type for wantStructuredBody: %T", tt.wantStructuredBody) + } + unmarshalResponse(t, w.Header().Get("Content-Type"), w.Body.Bytes(), got) + if diff := cmp.Diff(tt.wantStructuredBody, got, timeEqual()); diff != "" { + t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) + } + if tt.wantWarning { + if !strings.Contains(w.Header().Get("Warning"), "deprecated") { + t.Errorf("expected deprecation warning in header, but got: %s", w.Header().Get("Warning")) + } + } + } else { + if !strings.Contains(string(w.Body.String()), tt.wantBody) { + t.Errorf("Unexpected response body:\n- want to contain: %s\n- got: %s", tt.wantBody, string(w.Body.String())) + } + } + } + }) + } +} + +func unmarshalResponse(t *testing.T, contentType string, body []byte, got interface{}) { + t.Helper() + switch { + case strings.Contains(contentType, "application/json"): + if err := json.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling JSON response: %v", err) + } + case strings.Contains(contentType, "application/cbor"): + if err := cbor.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling CBOR response: %v", err) + } + case strings.Contains(contentType, "application/yaml"): + if err := yaml.Unmarshal(body, got); err != nil { + t.Fatalf("unexpected error while unmarshalling YAML response: %v", err) + } + default: + t.Fatalf("unexpected content type: %s", contentType) + } +} + +func parseVersion(t *testing.T, v string) *version.Version { + parsed, err := version.ParseMajorMinor(v) + if err != nil { + t.Fatalf("error parsing binary version: %s", v) + } + + return parsed +} + +type fakeRegistry struct { + startTime time.Time + goVer string + binaryVer *version.Version + emulationVer *version.Version + listedPaths []string + deprecated map[string]bool +} + +func (f fakeRegistry) deprecatedVersions() map[string]bool { + return f.deprecated +} + +func (f fakeRegistry) processStartTime() time.Time { + return f.startTime +} + +func (f fakeRegistry) goVersion() string { + return f.goVer +} + +func (f fakeRegistry) binaryVersion() *version.Version { + return f.binaryVer +} + +func (f fakeRegistry) emulationVersion() *version.Version { + return f.emulationVer +} + +func (f fakeRegistry) paths() []string { + return f.listedPaths +} + +func timeEqual() cmp.Option { + return cmp.Comparer(func(expectedTime, actualTime metav1.Time) bool { + return expectedTime.Truncate(time.Second).Equal(actualTime.Truncate(time.Second)) + }) +} + +// TestNewStatuszCodecFactory ensures all media types in the codec factory +// are explicitly handled. If this test fails, a new media type was added +// to the codec factory and needs to be explicitly added to the supported +// or unsupported list in newStatuszCodecFactory. +func TestNewStatuszCodecFactory(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CBORServingAndStorage, true) + scheme := runtime.NewScheme() + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) + + _, err := newStatuszCodecFactory(scheme, "", nil) + if err != nil { + t.Fatalf("unknown media type(s) detected - update newStatuszCodecFactory to explicitly handle them: %v", err) + } +} + +func TestMetrics(t *testing.T) { + fakeStartTime := time.Now() + fakeBinaryVersion := parseVersion(t, "1.31") + fakeEmulationVersion := parseVersion(t, "1.30") + fakeListedPaths := []string{"/livez/ping", "/readyz/ping"} + + reg := fakeRegistry{ + startTime: fakeStartTime, + goVer: "1.21", + binaryVer: fakeBinaryVersion, + emulationVer: fakeEmulationVersion, + listedPaths: fakeListedPaths, + } + + mux := http.NewServeMux() + Install(mux, "test-server", reg) + metrics.Register() + metrics.Reset() + + // text/plain request: group and version should be empty + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultStatuszPath), nil) + if err != nil { + t.Errorf("%v", err) + } + mux.ServeHTTP(httptest.NewRecorder(), req) + + // structured request: group and version should reflect the negotiated version + req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", DefaultStatuszPath), nil) + if err != nil { + t.Errorf("%v", err) + } + req.Header.Set("Accept", "application/json;v=v1alpha1;g=config.k8s.io;as=Statusz") + mux.ServeHTTP(httptest.NewRecorder(), req) + + expected := strings.NewReader(` + # HELP apiserver_request_total [STABLE] Counter of apiserver requests broken out for each verb, dry run value, group, version, resource, scope, component, and HTTP response code. + # TYPE apiserver_request_total counter + apiserver_request_total{code="200",component="test-server",dry_run="",group="",resource="statusz",scope="",subresource="",verb="GET",version=""} 1 + apiserver_request_total{code="200",component="test-server",dry_run="",group="config.k8s.io",resource="statusz",scope="",subresource="",verb="GET",version="v1alpha1"} 1 +`) + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, expected, "apiserver_request_total"); err != nil { + t.Error(err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/testing/testing.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/testing/testing.go new file mode 100644 index 000000000..75d9782a3 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/testing/testing.go @@ -0,0 +1,116 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apiserver/pkg/server/statusz/api/v1alpha1" + "k8s.io/apiserver/pkg/server/statusz/api/v1beta1" +) + +// VerifyStructuredResponse verifies that the response body matches the expected static fields +// and checks for the presence/absence of warnings. +func VerifyStructuredResponse(t *testing.T, acceptHeader string, body []byte, warnings []string, want interface{}, wantDeprecationHeader bool) { + t.Helper() + + unmarshal := unmarshalFunc(t, acceptHeader) + wantTypeMeta, wantName, wantPaths := wantFields(t, want) + gotTypeMeta, gotName, gotPaths := gotFields(t, unmarshal, body, wantTypeMeta.APIVersion) + + if gotName != wantName { + t.Errorf("ObjectMeta.Name mismatch: want %q, got %q", wantName, gotName) + } + if gotTypeMeta != wantTypeMeta { + t.Errorf("TypeMeta mismatch: want %+v, got %+v", wantTypeMeta, gotTypeMeta) + } + if diff := cmp.Diff(wantPaths, gotPaths); diff != "" { + t.Errorf("Paths mismatch (-want,+got):\n%s", diff) + } + + foundWarning := false + for _, w := range warnings { + if strings.Contains(w, "deprecated") { + foundWarning = true + break + } + } + + if wantDeprecationHeader { + if !foundWarning { + t.Errorf("want warning 'deprecated', got none or different: %v", warnings) + } + } else { + if foundWarning { + t.Errorf("want no warning 'deprecated', got one: %v", warnings) + } + } +} + +func unmarshalFunc(t *testing.T, acceptHeader string) func([]byte, interface{}) error { + switch { + case strings.Contains(acceptHeader, "application/json"): + return json.Unmarshal + case strings.Contains(acceptHeader, "application/yaml"): + return yaml.Unmarshal + case strings.Contains(acceptHeader, "application/cbor"): + return cbor.Unmarshal + default: + t.Fatalf("unexpected Accept header: %q", acceptHeader) + } + return nil +} + +func wantFields(t *testing.T, want interface{}) (metav1.TypeMeta, string, []string) { + t.Helper() + switch w := want.(type) { + case *v1alpha1.Statusz: + return w.TypeMeta, w.Name, w.Paths + case *v1beta1.Statusz: + return w.TypeMeta, w.Name, w.Paths + default: + t.Fatalf("unexpected type: %T", want) + } + return metav1.TypeMeta{}, "", nil +} + +func gotFields(t *testing.T, unmarshal func([]byte, interface{}) error, body []byte, apiVersion string) (metav1.TypeMeta, string, []string) { + t.Helper() + switch apiVersion { + case "config.k8s.io/v1alpha1": + var got v1alpha1.Statusz + if err := unmarshal(body, &got); err != nil { + t.Fatalf("error unmarshalling %s: %v", apiVersion, err) + } + return got.TypeMeta, got.Name, got.Paths + case "config.k8s.io/v1beta1": + var got v1beta1.Statusz + if err := unmarshal(body, &got); err != nil { + t.Fatalf("error unmarshalling %s: %v", apiVersion, err) + } + return got.TypeMeta, got.Name, got.Paths + default: + t.Fatalf("unexpected APIVersion: %q", apiVersion) + } + return metav1.TypeMeta{}, "", nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/textserializer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/textserializer.go new file mode 100644 index 000000000..5c97923a0 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/statusz/textserializer.go @@ -0,0 +1,88 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statusz + +import ( + "fmt" + "html" + "io" + "math/rand" + "strings" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + v1beta1 "k8s.io/apiserver/pkg/server/statusz/api/v1beta1" +) + +// statuszTextSerializer implements runtime.Serializer for text/plain output. +type statuszTextSerializer struct { + componentName string + reg statuszRegistry +} + +// Encode writes the statusz information in plain text format to the given writer, using the provided obj. +func (s statuszTextSerializer) Encode(obj runtime.Object, w io.Writer) error { + if _, err := fmt.Fprintf(w, headerFmt, s.componentName); err != nil { + return err + } + + randomIndex := rand.Intn(len(delimiters)) + delim := html.EscapeString(delimiters[randomIndex]) + + statuszObj, ok := obj.(*v1beta1.Statusz) + if !ok { + return fmt.Errorf("expected *v1beta1.Statusz, got %T", obj) + } + + startTime := html.EscapeString(statuszObj.StartTime.Time.Format(time.UnixDate)) + uptimeStr := html.EscapeString(uptime(statuszObj.StartTime.Time)) + goVersion := html.EscapeString(statuszObj.GoVersion) + binaryVersion := html.EscapeString(statuszObj.BinaryVersion) + + var emulationVersion string + if statuszObj.EmulationVersion != "" { + emulationVersion = fmt.Sprintf(`Emulation version%s %s`, delim, html.EscapeString(statuszObj.EmulationVersion)) + } + + paths := strings.Join(statuszObj.Paths, " ") + if paths != "" { + paths = fmt.Sprintf(`Paths%s %s`, delim, html.EscapeString(paths)) + } + + status := fmt.Sprintf(` +Started%[1]s %[2]s +Up%[1]s %[3]s +Go version%[1]s %[4]s +Binary version%[1]s %[5]s +%[6]s +%[7]s +`, delim, startTime, uptimeStr, goVersion, binaryVersion, emulationVersion, paths) + _, err := fmt.Fprint(w, status) + return err +} + +// Decode is not supported for text/plain serialization. +func (s statuszTextSerializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + return nil, nil, fmt.Errorf("decode not supported for text/plain") +} + +// Identifier returns a unique identifier for this serializer. +func (s statuszTextSerializer) Identifier() runtime.Identifier { + return runtime.Identifier("statuszTextSerializer") +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_config_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_config_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_config_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_config_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_encoding_config.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_encoding_config.go index 142240b8a..981326666 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/resource_encoding_config.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/resource_encoding_config.go @@ -122,10 +122,6 @@ type introducedInterface interface { APILifecycleIntroduced() (major, minor int) } -type replacementInterface interface { - APILifecycleReplacement() schema.GroupVersionKind -} - func emulatedStorageVersion(binaryVersionOfResource schema.GroupVersion, example runtime.Object, effectiveVersion basecompatibility.EffectiveVersion, scheme *runtime.Scheme) (schema.GroupVersion, error) { if example == nil || effectiveVersion == nil { return binaryVersionOfResource, nil @@ -179,14 +175,6 @@ func emulatedStorageVersion(binaryVersionOfResource schema.GroupVersion, example // If it was introduced after current compatibility version, don't use it // skip the introduced check for test when current compatibility version is 0.0 to test all apis if introduced, hasIntroduced := exampleOfGVK.(introducedInterface); hasIntroduced && (compatibilityVersion.Major() > 0 || compatibilityVersion.Minor() > 0) { - - // Skip versions that have a replacement. - // This can be used to override this storage version selection by - // marking a storage version has having a replacement and preventing a - // that storage version from being selected. - if _, hasReplacement := exampleOfGVK.(replacementInterface); hasReplacement { - continue - } // API resource lifecycles should be relative to k8s api version majorIntroduced, minorIntroduced := introduced.APILifecycleIntroduced() introducedVer := apimachineryversion.MajorMinor(uint(majorIntroduced), uint(minorIntroduced)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_codec.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_codec.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_codec.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_codec.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_factory.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_factory.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_factory.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_factory.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_factory_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_factory_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage/storage_factory_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage/storage_factory_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage_readiness_hook.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage_readiness_hook.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage_readiness_hook.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage_readiness_hook.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage_readiness_hook_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage_readiness_hook_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/server/storage_readiness_hook_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/server/storage_readiness_hook_test.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser.go new file mode 100644 index 000000000..fe08eac0c --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser.go @@ -0,0 +1,217 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sharding + +import ( + "fmt" + "strings" + + celparser "github.com/google/cel-go/parser" + + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/ast" + "github.com/google/cel-go/common/operators" + + apisharding "k8s.io/apimachinery/pkg/sharding" +) + +// Parse parses a CEL-based shard selector expression into a Selector. +// +// The expression format is: +// +// shardRange(object.metadata.uid, '0x0', '0x8000000000000000') +// shardRange(object.metadata.uid, '0x0', '0x8000000000000000') || shardRange(...) +// +// Only the shardRange() function and || operator are permitted. The CEL +// expression is parsed but never evaluated — the AST is walked to extract +// shard range requirements. +func Parse(expr string) (apisharding.Selector, error) { + expr = strings.TrimSpace(expr) + if expr == "" { + return nil, fmt.Errorf("empty shard selector is not allowed; omit the parameter for unfiltered lists") + } + + p, err := celparser.NewParser(celparser.Macros( /* no macros */ )) + if err != nil { + return nil, fmt.Errorf("failed to create CEL parser: %w", err) + } + + parsed, errs := p.Parse(common.NewTextSource(expr)) + if errs != nil && len(errs.GetErrors()) > 0 { + return nil, fmt.Errorf("CEL parse error: %s", errs.GetErrors()[0].Message) + } + + reqs, err := walkExpr(parsed.Expr()) + if err != nil { + return nil, err + } + + // Validate that all requirements use the same field key. + for i := 1; i < len(reqs); i++ { + if reqs[i].Key != reqs[0].Key { + return nil, fmt.Errorf("all shard ranges must use the same field, got %q and %q", reqs[0].Key, reqs[i].Key) + } + } + + return apisharding.NewSelector(reqs...), nil +} + +// walkExpr recursively walks the CEL AST and extracts ShardRangeRequirement values. +// The root expression must be either a shardRange() call or a chain of || operators +// combining shardRange() calls. +func walkExpr(e ast.Expr) ([]apisharding.ShardRangeRequirement, error) { + if e.Kind() == ast.CallKind { + call := e.AsCall() + fn := call.FunctionName() + + if fn == operators.LogicalOr { + // _||_ operator: recurse into both sides + args := call.Args() + if len(args) != 2 { + return nil, fmt.Errorf("|| operator requires exactly 2 arguments") + } + left, err := walkExpr(args[0]) + if err != nil { + return nil, err + } + right, err := walkExpr(args[1]) + if err != nil { + return nil, err + } + return append(left, right...), nil + } + + if fn == "shardRange" { + req, err := parseShardRangeCall(call) + if err != nil { + return nil, err + } + return []apisharding.ShardRangeRequirement{req}, nil + } + + return nil, fmt.Errorf("unsupported function %q; only shardRange() and || are allowed", fn) + } + + return nil, fmt.Errorf("unexpected expression kind %v; expected shardRange() call or || operator", e.Kind()) +} + +// parseShardRangeCall extracts a ShardRangeRequirement from a shardRange(field, start, end) call. +func parseShardRangeCall(call ast.CallExpr) (apisharding.ShardRangeRequirement, error) { + args := call.Args() + if len(args) != 3 { + return apisharding.ShardRangeRequirement{}, fmt.Errorf("shardRange() requires exactly 3 arguments, got %d", len(args)) + } + + // Arg 0: field path (select chain like object.metadata.uid) + fieldPath, err := extractFieldPath(args[0]) + if err != nil { + return apisharding.ShardRangeRequirement{}, fmt.Errorf("shardRange() first argument: %w", err) + } + + // Validate field path + switch fieldPath { + case "object.metadata.uid", "object.metadata.namespace": + // ok + default: + return apisharding.ShardRangeRequirement{}, fmt.Errorf("unsupported field path %q; supported: object.metadata.uid, object.metadata.namespace", fieldPath) + } + + // Arg 1: hex start (string literal) + hexStart, err := extractHexLiteral(args[1], "hexStart") + if err != nil { + return apisharding.ShardRangeRequirement{}, err + } + + // Arg 2: hex end (string literal) + hexEnd, err := extractHexLiteral(args[2], "hexEnd") + if err != nil { + return apisharding.ShardRangeRequirement{}, err + } + + // Validate start < end using the canonical hex comparison. + if !apisharding.HexLess(hexStart, hexEnd) { + return apisharding.ShardRangeRequirement{}, fmt.Errorf("shard range start %s must be less than end %s", hexStart, hexEnd) + } + + return apisharding.ShardRangeRequirement{ + Key: fieldPath, + Start: hexStart, + End: hexEnd, + }, nil +} + +// extractFieldPath walks a select chain (object.metadata.uid) and returns the dot-joined path. +func extractFieldPath(e ast.Expr) (string, error) { + switch e.Kind() { + case ast.IdentKind: + return e.AsIdent(), nil + case ast.SelectKind: + sel := e.AsSelect() + operand, err := extractFieldPath(sel.Operand()) + if err != nil { + return "", err + } + return operand + "." + sel.FieldName(), nil + default: + return "", fmt.Errorf("expected field path (e.g. object.metadata.uid), got expression kind %v", e.Kind()) + } +} + +// extractHexLiteral extracts a hex string from a CEL string literal. +// The literal must be a single-quoted string like '0xff' with a 0x prefix. +func extractHexLiteral(e ast.Expr, name string) (string, error) { + if e.Kind() != ast.LiteralKind { + return "", fmt.Errorf("%s must be a string literal (e.g. '0xff'), got expression kind %v", name, e.Kind()) + } + + val := e.AsLiteral() + s, ok := val.Value().(string) + if !ok { + return "", fmt.Errorf("%s must be a string literal, got %T", name, val.Value()) + } + + if !strings.HasPrefix(s, "0x") { + return "", fmt.Errorf("%s must have '0x' prefix, got %q", name, s) + } + + hex := s[2:] + if hex == "" { + return "", fmt.Errorf("%s: hex value is required after '0x'", name) + } + if len(hex) > 17 { + return "", fmt.Errorf("%s: hex value too long (%d chars, max 17): %q", name, len(hex), hex) + } + + for _, c := range hex { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') { + return "", fmt.Errorf("%s: invalid hex character %q in %q", name, string(c), s) + } + } + + // Require exactly 16 hex digits, except for the special case + // "0x10000000000000000" (2^64) which is the exclusive upper bound. + if len(hex) == 17 { + if s != "0x10000000000000000" { + return "", fmt.Errorf("%s: 17-digit hex value must be '0x10000000000000000' (2^64), got %q", name, s) + } + } else if len(hex) != 16 { + padded := "0x" + strings.Repeat("0", 16-len(hex)) + hex + return "", fmt.Errorf("%s must be a 0x-prefixed 16-digit hex value, got %q (did you mean %q?)", name, s, padded) + } + + return s, nil +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser_test.go new file mode 100644 index 000000000..bf412a45b --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/sharding/parser_test.go @@ -0,0 +1,237 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sharding + +import ( + "testing" + + apisharding "k8s.io/apimachinery/pkg/sharding" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + wantReq []apisharding.ShardRangeRequirement + }{ + { + name: "empty string", + input: "", + wantErr: true, + }, + { + name: "single requirement with uid", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x8000000000000000"}, + }, + }, + { + name: "full range with 2^64 end", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0x10000000000000000')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x10000000000000000"}, + }, + }, + { + name: "namespace field", + input: "shardRange(object.metadata.namespace, '0x00000000000000aa', '0x00000000000000ff')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.namespace", Start: "0x00000000000000aa", End: "0x00000000000000ff"}, + }, + }, + { + name: "two requirements with ||", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000') || shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x8000000000000000"}, + {Key: "object.metadata.uid", Start: "0x8000000000000000", End: "0x10000000000000000"}, + }, + }, + { + name: "three requirements with ||", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0x0000000000000005') || shardRange(object.metadata.uid, '0x0000000000000005', '0x000000000000000a') || shardRange(object.metadata.uid, '0x000000000000000a', '0x000000000000000f')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x0000000000000005"}, + {Key: "object.metadata.uid", Start: "0x0000000000000005", End: "0x000000000000000a"}, + {Key: "object.metadata.uid", Start: "0x000000000000000a", End: "0x000000000000000f"}, + }, + }, + { + name: "no spaces", + input: "shardRange(object.metadata.uid,'0x0000000000000000','0x8000000000000000')", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x8000000000000000"}, + }, + }, + { + name: "extra whitespace", + input: " shardRange( object.metadata.uid , '0x0000000000000000' , '0x8000000000000000' ) ", + wantReq: []apisharding.ShardRangeRequirement{ + {Key: "object.metadata.uid", Start: "0x0000000000000000", End: "0x8000000000000000"}, + }, + }, + // Error cases + { + name: "missing 0x prefix on start", + input: "shardRange(object.metadata.uid, '0000000000000000', '0x8000000000000000')", + wantErr: true, + }, + { + name: "missing 0x prefix on end", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '8000000000000000')", + wantErr: true, + }, + { + name: "empty after 0x prefix", + input: "shardRange(object.metadata.uid, '0x', '0x8000000000000000')", + wantErr: true, + }, + { + name: "name field unsupported", + input: "shardRange(object.metadata.name, '0x00', '0x80')", + wantErr: true, + }, + { + name: "unsupported field", + input: "shardRange(object.metadata.labels, '0x00', '0x80')", + wantErr: true, + }, + { + name: "unknown function", + input: "invalidFunc(object.metadata.uid, '0x00', '0x80')", + wantErr: true, + }, + { + name: "hex too long (18 chars)", + input: "shardRange(object.metadata.uid, '0x000000000000000000', '0x80')", + wantErr: true, + }, + { + name: "invalid hex char", + input: "shardRange(object.metadata.uid, '0x0g', '0x80')", + wantErr: true, + }, + { + name: "&& operator not allowed", + input: "shardRange(object.metadata.uid, '0x0', '0x8') && shardRange(object.metadata.uid, '0x8', '0xf')", + wantErr: true, + }, + { + name: "integer literal instead of string", + input: "shardRange(object.metadata.uid, 0, 8)", + wantErr: true, + }, + { + name: "comparison operator", + input: "object.metadata.uid > '0x0'", + wantErr: true, + }, + { + name: "wrong number of arguments", + input: "shardRange(object.metadata.uid, '0x0')", + wantErr: true, + }, + { + name: "nested function call", + input: "shardRange(object.metadata.uid, hex(0), '0x8')", + wantErr: true, + }, + { + name: "mixed field keys", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000') || shardRange(object.metadata.namespace, '0x8000000000000000', '0x10000000000000000')", + wantErr: true, + }, + { + name: "short hex rejected", + input: "shardRange(object.metadata.uid, '0x0', '0x8000000000000000')", + wantErr: true, + }, + { + name: "short hex rejected on end", + input: "shardRange(object.metadata.uid, '0x0000000000000000', '0xff')", + wantErr: true, + }, + { + name: "start equals end", + input: "shardRange(object.metadata.uid, '0x8000000000000000', '0x8000000000000000')", + wantErr: true, + }, + { + name: "start greater than end", + input: "shardRange(object.metadata.uid, '0xffffffffffffffff', '0x0000000000000000')", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sel, err := Parse(tt.input) + if tt.wantErr { + if err == nil { + t.Errorf("Parse(%q) expected error, got nil", tt.input) + } + return + } + if err != nil { + t.Fatalf("Parse(%q) unexpected error: %v", tt.input, err) + } + + reqs := sel.Requirements() + if len(reqs) != len(tt.wantReq) { + t.Fatalf("Parse(%q) got %d requirements, want %d", tt.input, len(reqs), len(tt.wantReq)) + } + for i, req := range reqs { + if req != tt.wantReq[i] { + t.Errorf("Parse(%q) requirement[%d] = %+v, want %+v", tt.input, i, req, tt.wantReq[i]) + } + } + }) + } +} + +func TestParseRoundTrip(t *testing.T) { + tests := []string{ + "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000')", + "shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')", + "shardRange(object.metadata.uid, '0x0000000000000000', '0x10000000000000000')", + "shardRange(object.metadata.namespace, '0x00000000000000aa', '0x00000000000000ff')", + "shardRange(object.metadata.uid, '0x0000000000000000', '0x8000000000000000') || shardRange(object.metadata.uid, '0x8000000000000000', '0x10000000000000000')", + } + + for _, input := range tests { + t.Run(input, func(t *testing.T) { + sel, err := Parse(input) + if err != nil { + t.Fatalf("Parse(%q) error: %v", input, err) + } + output := sel.String() + if output != input { + t.Errorf("Parse(%q).String() = %q, want %q", input, output, input) + } + // Round-trip — parse(string(parse(input))) should be stable. + sel2, err := Parse(output) + if err != nil { + t.Fatalf("Parse(%q) (round-trip) error: %v", output, err) + } + if sel.String() != sel2.String() { + t.Errorf("round-trip unstable: %q -> %q -> %q", input, output, sel2.String()) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/api_object_versioner.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/api_object_versioner.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/api_object_versioner.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/api_object_versioner.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/api_object_versioner_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/api_object_versioner_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/api_object_versioner_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/api_object_versioner_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher.go index a74cf8d4a..8711596a3 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher.go @@ -371,10 +371,10 @@ func (c *cacheWatcher) convertToWatchEvent(event *watchCacheEvent) *watch.Event return e } - curObjPasses := event.Type != watch.Deleted && c.filter(event.Key, event.ObjLabels, event.ObjFields) + curObjPasses := event.Type != watch.Deleted && c.filter(event.Key, event.ObjLabels, event.ObjFields, event.Object) oldObjPasses := false if event.PrevObject != nil { - oldObjPasses = c.filter(event.Key, event.PrevObjLabels, event.PrevObjFields) + oldObjPasses = c.filter(event.Key, event.PrevObjLabels, event.PrevObjFields, event.PrevObject) } if !curObjPasses && !oldObjPasses { // Watcher is not interested in that object. @@ -434,7 +434,7 @@ func (c *cacheWatcher) sendWatchCacheEvent(event *watchCacheEvent) { } func (c *cacheWatcher) processInterval(ctx context.Context, cacheInterval *watchCacheInterval, resourceVersion uint64) { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) defer close(c.result) defer c.Stop() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher_test.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher_test.go index 85d44df52..7d68dfbc6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cache_watcher_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cache_watcher_test.go @@ -29,13 +29,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/storage" + "k8s.io/apiserver/pkg/storage/cacher/store" utilflowcontrol "k8s.io/apiserver/pkg/util/flowcontrol" "k8s.io/client-go/tools/cache" testingclock "k8s.io/utils/clock/testing" + + cachertesting "k8s.io/apiserver/pkg/storage/cacher/testing" ) // verifies the cacheWatcher.process goroutine is properly cleaned up even if @@ -44,7 +48,7 @@ func TestCacheWatcherCleanupNotBlockedByResult(t *testing.T) { var lock sync.RWMutex var w *cacheWatcher count := 0 - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { lock.Lock() defer lock.Unlock() @@ -74,7 +78,7 @@ func TestCacheWatcherCleanupNotBlockedByResult(t *testing.T) { } func TestCacheWatcherHandlesFiltering(t *testing.T) { - filter := func(_ string, _ labels.Set, field fields.Set) bool { + filter := func(_ string, _ labels.Set, field fields.Set, _ runtime.Object) bool { return field["spec.nodeName"] == "host" } forget := func(bool) {} @@ -206,7 +210,7 @@ TestCase: func TestCacheWatcherStoppedInAnotherGoroutine(t *testing.T) { var w *cacheWatcher done := make(chan struct{}) - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { w.setDrainInputBufferLocked(drainWatcher) w.stopLocked() @@ -246,7 +250,7 @@ func TestCacheWatcherStoppedInAnotherGoroutine(t *testing.T) { } func TestCacheWatcherStoppedOnDestroy(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -258,7 +262,7 @@ func TestCacheWatcherStoppedOnDestroy(t *testing.T) { t.Fatalf("unexpected error waiting for the cache to be ready") } - w, err := cacher.Watch(context.Background(), "pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) + w, err := cacher.Watch(context.Background(), "/pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -288,7 +292,7 @@ func TestCacheWatcherStoppedOnDestroy(t *testing.T) { func TestResourceVersionAfterInitEvents(t *testing.T) { const numObjects = 10 - store := cache.NewIndexer(storeElementKey, storeElementIndexers(nil)) + store := cache.NewIndexer(store.ElementKey, store.ElementIndexers(nil)) for i := 0; i < numObjects; i++ { elem := makeTestStoreElement(makeTestPod(fmt.Sprintf("pod-%d", i), uint64(i))) @@ -300,7 +304,7 @@ func TestResourceVersionAfterInitEvents(t *testing.T) { t.Fatal(err) } - filter := func(_ string, _ labels.Set, _ fields.Set) bool { return true } + filter := func(_ string, _ labels.Set, _ fields.Set, _ runtime.Object) bool { return true } forget := func(_ bool) {} deadline := time.Now().Add(time.Minute) w := newCacheWatcher(numObjects+1, filter, forget, storage.APIObjectVersioner{}, deadline, true, schema.GroupResource{Resource: "pods"}, "") @@ -340,7 +344,7 @@ func TestResourceVersionAfterInitEvents(t *testing.T) { } func TestTimeBucketWatchersBasic(t *testing.T) { - filter := func(_ string, _ labels.Set, _ fields.Set) bool { + filter := func(_ string, _ labels.Set, _ fields.Set, _ runtime.Object) bool { return true } forget := func(bool) {} @@ -401,7 +405,7 @@ func TestCacheWatcherDraining(t *testing.T) { var lock sync.RWMutex var w *cacheWatcher count := 0 - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { lock.Lock() defer lock.Unlock() @@ -442,7 +446,7 @@ func TestCacheWatcherDrainingRequestedButNotDrained(t *testing.T) { var lock sync.RWMutex var w *cacheWatcher count := 0 - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { lock.Lock() defer lock.Unlock() @@ -476,7 +480,7 @@ func TestCacheWatcherDrainingNoBookmarkAfterResourceVersionReceived(t *testing.T var lock sync.RWMutex var w *cacheWatcher count := 0 - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { lock.Lock() defer lock.Unlock() @@ -540,7 +544,7 @@ func TestCacheWatcherDrainingNoBookmarkAfterResourceVersionSent(t *testing.T) { watchInitializationSignal := utilflowcontrol.NewInitializationSignal() ctx := utilflowcontrol.WithInitializationSignal(context.Background(), watchInitializationSignal) count := 0 - filter := func(string, labels.Set, fields.Set) bool { return true } + filter := func(string, labels.Set, fields.Set, runtime.Object) bool { return true } forget := func(drainWatcher bool) { lock.Lock() defer lock.Unlock() @@ -603,7 +607,7 @@ func TestCacheWatcherDrainingNoBookmarkAfterResourceVersionSent(t *testing.T) { func TestBookmarkAfterResourceVersionWatchers(t *testing.T) { newWatcher := func(id string, deadline time.Time) *cacheWatcher { - w := newCacheWatcher(0, func(_ string, _ labels.Set, _ fields.Set) bool { return true }, func(bool) {}, storage.APIObjectVersioner{}, deadline, true, schema.GroupResource{Resource: "pods"}, id) + w := newCacheWatcher(0, func(_ string, _ labels.Set, _ fields.Set, _ runtime.Object) bool { return true }, func(bool) {}, storage.APIObjectVersioner{}, deadline, true, schema.GroupResource{Resource: "pods"}, id) w.setBookmarkAfterResourceVersion(10) return w } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher.go index 3770bc837..326ed27c8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher.go @@ -36,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/audit" @@ -45,6 +46,7 @@ import ( "k8s.io/apiserver/pkg/storage/cacher/delegator" "k8s.io/apiserver/pkg/storage/cacher/metrics" "k8s.io/apiserver/pkg/storage/cacher/progress" + "k8s.io/apiserver/pkg/storage/cacher/store" etcdfeature "k8s.io/apiserver/pkg/storage/feature" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" @@ -246,7 +248,7 @@ func (t *watcherBookmarkTimeBuckets) popExpiredWatchersThreadUnsafe() [][]*cache return expiredWatchers } -type filterWithAttrsFunc func(key string, l labels.Set, f fields.Set) bool +type filterWithAttrsFunc func(key string, l labels.Set, f fields.Set, obj runtime.Object) bool type indexedTriggerFunc struct { indexName string @@ -374,8 +376,18 @@ func NewCacherFromConfig(config Config) (*Cacher, error) { config.Clock = clock.RealClock{} } objType := reflect.TypeOf(obj) + resourcePrefix := config.ResourcePrefix + if resourcePrefix == "" { + return nil, fmt.Errorf("resourcePrefix cannot be empty") + } + if resourcePrefix == "/" { + return nil, fmt.Errorf("resourcePrefix cannot be /") + } + if !strings.HasPrefix(resourcePrefix, "/") { + return nil, fmt.Errorf("resourcePrefix needs to start from /") + } cacher := &Cacher{ - resourcePrefix: config.ResourcePrefix, + resourcePrefix: resourcePrefix, ready: newReady(config.Clock), storage: config.Storage, objectType: objType, @@ -426,8 +438,8 @@ func NewCacherFromConfig(config Config) (*Cacher, error) { watchCache := newWatchCache( config.KeyFunc, cacher.processEvent, config.GetAttrsFunc, config.Versioner, config.Indexers, config.Clock, eventFreshDuration, config.GroupResource, progressRequester, config.Storage.GetCurrentResourceVersion) - listerWatcher := NewListerWatcher(config.Storage, config.ResourcePrefix, config.NewListFunc, contextMetadata) - reflectorName := "storage/cacher.go:" + config.ResourcePrefix + listerWatcher := NewListerWatcher(config.Storage, resourcePrefix, config.NewListFunc, contextMetadata) + reflectorName := "storage/cacher.go:" + resourcePrefix reflector := cache.NewNamedReflector(reflectorName, listerWatcher, obj, watchCache, 0) // Configure reflector's pager to for an appropriate pagination chunk size for fetching data from @@ -441,6 +453,13 @@ func NewCacherFromConfig(config Config) (*Cacher, error) { cacher.watchCache = watchCache cacher.reflector = reflector + if utilfeature.DefaultFeatureGate.Enabled(features.SizeBasedListCostEstimate) { + err := config.Storage.EnableResourceSizeEstimation(cacher.getKeys) + if err != nil { + return nil, fmt.Errorf("failed to enable resource size estimation: %w", err) + } + } + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { cacher.compactor = newCompactor(config.Storage, watchCache, config.Clock) go cacher.compactor.Run(stopCh) @@ -461,7 +480,6 @@ func NewCacherFromConfig(config Config) (*Cacher, error) { }, time.Second, stopCh, ) }() - config.Storage.SetKeysFunc(cacher.getKeys) return cacher, nil } @@ -489,6 +507,14 @@ type namespacedName struct { } func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + ctx, span := tracing.Start(ctx, "cacher.Watch", + attribute.String("audit-id", audit.GetAuditIDTruncated(ctx)), + attribute.Stringer("type", c.groupResource)) + defer span.End(500 * time.Millisecond) + key, err := c.prepareKey(key, opts.Recursive) + if err != nil { + return nil, err + } pred := opts.Predicate requestedWatchRV, err := c.versioner.ParseResourceVersion(opts.ResourceVersion) if err != nil { @@ -577,7 +603,7 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions // to compute watcher.forget function (which has to happen under lock). watcher := newCacheWatcher( chanSize, - filterWithAttrsAndPrefixFunction(key, pred), + filterWithAttrsAndPrefixFunction(key, pred, c.groupResource), emptyFunc, c.versioner, deadline, @@ -651,11 +677,24 @@ func (c *Cacher) Watch(ctx context.Context, key string, opts storage.ListOptions return newImmediateCloseWatcher(), nil } + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) && pred.ShardSelector != nil && !pred.ShardSelector.Empty() { + metrics.RecordShardedWatchStarted(c.groupResource) + originalForget := watcher.forget + watcher.forget = func(drainWatcher bool) { + metrics.RecordShardedWatchStopped(c.groupResource) + originalForget(drainWatcher) + } + } + go watcher.processInterval(ctx, cacheInterval, requiredResourceVersion) return watcher, nil } func (c *Cacher) Get(ctx context.Context, key string, opts storage.GetOptions, objPtr runtime.Object) error { + key, err := c.prepareKey(key, false) + if err != nil { + return err + } getRV, err := c.versioner.ParseResourceVersion(opts.ResourceVersion) if err != nil { return err @@ -672,9 +711,9 @@ func (c *Cacher) Get(ctx context.Context, key string, opts storage.GetOptions, o } if exists { - elem, ok := obj.(*storeElement) + elem, ok := obj.(*store.Element) if !ok { - return fmt.Errorf("non *storeElement returned from storage: %v", obj) + return fmt.Errorf("non *store.Element returned from storage: %v", obj) } objVal.Set(reflect.ValueOf(elem.Object).Elem()) } else { @@ -707,15 +746,11 @@ type listResp struct { // GetList implements storage.Interface func (c *Cacher) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { - // For recursive lists, we need to make sure the key ended with "/" so that we only - // get children "directories". e.g. if we have key "/a", "/a/b", "/ab", getting keys - // with prefix "/a" will return all three, while with prefix "/a/" will return only - // "/a/b" which is the correct answer. - preparedKey := key - if opts.Recursive && !strings.HasSuffix(key, "/") { - preparedKey += "/" - } - _, err := c.versioner.ParseResourceVersion(opts.ResourceVersion) + preparedKey, err := c.prepareKey(key, opts.Recursive) + if err != nil { + return err + } + _, err = c.versioner.ParseResourceVersion(opts.ResourceVersion) if err != nil { return err } @@ -765,11 +800,19 @@ func (c *Cacher) GetList(ctx context.Context, key string, opts storage.ListOptio var hasMoreListItems bool limit := computeListLimit(opts) for i, obj := range resp.Items { - elem, ok := obj.(*storeElement) + elem, ok := obj.(*store.Element) if !ok { - return fmt.Errorf("non *storeElement returned from storage: %v", obj) + return fmt.Errorf("non *store.Element returned from storage: %v", obj) } - if opts.Predicate.MatchesObjectAttributes(elem.Labels, elem.Fields) { + shardMatch := true + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + var err error + shardMatch, err = opts.Predicate.MatchesSharding(elem.Object) + if err != nil { + return fmt.Errorf("shard matching failed: %w", err) + } + } + if shardMatch && opts.Predicate.MatchesObjectAttributes(elem.Labels, elem.Fields) { selectedObjects = append(selectedObjects, elem.Object) lastSelectedObjectKey = elem.Key } @@ -800,6 +843,9 @@ func (c *Cacher) GetList(ctx context.Context, key string, opts storage.ListOptio return err } } + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + opts.Predicate.SetShardInfoOnList(listObj) + } metrics.RecordListCacheMetrics(c.groupResource, indexUsed, len(resp.Items), listVal.Len()) return nil } @@ -1159,6 +1205,10 @@ func (c *Cacher) Stop() { c.stopWg.Wait() } +func (c *Cacher) prepareKey(key string, recursive bool) (string, error) { + return storage.PrepareKey(c.resourcePrefix, key, recursive) +} + func forgetWatcher(c *Cacher, w *cacheWatcher, index int, scope namespacedName, triggerValue string, triggerSupported bool) func(bool) { return func(drainWatcher bool) { c.Lock() @@ -1174,11 +1224,23 @@ func forgetWatcher(c *Cacher, w *cacheWatcher, index int, scope namespacedName, } } -func filterWithAttrsAndPrefixFunction(key string, p storage.SelectionPredicate) filterWithAttrsFunc { - filterFunc := func(objKey string, label labels.Set, field fields.Set) bool { +func filterWithAttrsAndPrefixFunction(key string, p storage.SelectionPredicate, groupResource schema.GroupResource) filterWithAttrsFunc { + isSharded := utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) && p.ShardSelector != nil && !p.ShardSelector.Empty() + filterFunc := func(objKey string, label labels.Set, field fields.Set, obj runtime.Object) bool { if !hasPathPrefix(objKey, key) { return false } + if isSharded { + matches, err := p.MatchesSharding(obj) + if err != nil { + utilruntime.HandleError(fmt.Errorf("shard matching failed for %v: %w", groupResource, err)) + return false + } + if !matches { + metrics.RecordWatchFilteredEvent(groupResource) + return false + } + } return p.MatchesObjectAttributes(label, field) } return filterFunc @@ -1214,6 +1276,7 @@ func (c *Cacher) getBookmarkAfterResourceVersionLockedFunc(parsedResourceVersion } } +// isListWatchRequest is mirrored in staging/src/k8s.io/apiserver/pkg/endpoints/handlers/get.go func isListWatchRequest(opts storage.ListOptions) bool { return opts.SendInitialEvents != nil && *opts.SendInitialEvents && opts.Predicate.AllowWatchBookmarks } @@ -1234,7 +1297,7 @@ func (c *Cacher) getWatchCacheResourceVersion(ctx context.Context, parsedWatchRe return parsedWatchResourceVersion, nil } // legacy case - if !utilfeature.DefaultFeatureGate.Enabled(features.WatchFromStorageWithoutResourceVersion) && opts.SendInitialEvents == nil && opts.ResourceVersion == "" { + if opts.SendInitialEvents == nil && opts.ResourceVersion == "" { return 0, nil } rv, err := c.storage.GetCurrentResourceVersion(ctx) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_test.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_test.go index 309851f5c..52d079fc8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_test.go @@ -42,6 +42,7 @@ import ( storagetesting "k8s.io/apiserver/pkg/storage/testing" "k8s.io/apiserver/pkg/storage/value/encrypt/identity" utilfeature "k8s.io/apiserver/pkg/util/feature" + clientfeatures "k8s.io/client-go/features" "k8s.io/client-go/tools/cache" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/klog/v2" @@ -199,14 +200,14 @@ func TestLists(t *testing.T) { t.Parallel() ctx, cacher, server, terminate := testSetupWithEtcdServer(t) t.Cleanup(terminate) - storagetesting.RunTestConsistentList(ctx, t, cacher, increaseRV(server.V3Client.Client), true, consistentRead, listFromCacheSnapshot) + storagetesting.RunTestConsistentList(ctx, t, cacher, increaseRVFunc(server.V3Client.Client), true, consistentRead, listFromCacheSnapshot) }) t.Run("GetListNonRecursive", func(t *testing.T) { t.Parallel() ctx, cacher, server, terminate := testSetupWithEtcdServer(t) t.Cleanup(terminate) - storagetesting.RunTestGetListNonRecursive(ctx, t, increaseRV(server.V3Client.Client), cacher) + storagetesting.RunTestGetListNonRecursive(ctx, t, increaseRVFunc(server.V3Client.Client), cacher) }) }) } @@ -218,7 +219,7 @@ func TestCompactRevision(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ListFromCacheSnapshot, true) ctx, cacher, server, terminate := testSetupWithEtcdServer(t) t.Cleanup(terminate) - storagetesting.RunTestCompactRevision(ctx, t, cacher, increaseRV(server.V3Client.Client), compactStore(cacher, server.V3Client.Client)) + storagetesting.RunTestCompactRevision(ctx, t, cacher, increaseRVFunc(server.V3Client.Client), compactStore(cacher, server.V3Client.Client)) } func TestMarkConsistent(t *testing.T) { @@ -303,7 +304,7 @@ func etcdListRequests(t *testing.T, ctx context.Context, store storage.Interface key := rand.String(10) listCtx := context.WithValue(ctx, storagetesting.RecorderContextKey, key) listOut := &example.PodList{} - if err := store.GetList(listCtx, "/pods", opts, listOut); err != nil { + if err := store.GetList(listCtx, "/pods/", opts, listOut); err != nil { t.Fatalf("Unexpected error: %v", err) } return recorder.ListRequestForKey(key) @@ -391,6 +392,11 @@ func TestStats(t *testing.T) { }) } } +func TestKeySchema(t *testing.T) { + ctx, cacher, terminate := testSetup(t) + t.Cleanup(terminate) + storagetesting.RunTestKeySchema(ctx, t, cacher) +} func TestWatch(t *testing.T) { ctx, cacher, terminate := testSetup(t) @@ -513,7 +519,7 @@ type setupOptions struct { type setupOption func(*setupOptions) func withDefaults(options *setupOptions) { - prefix := "/pods" + prefix := "/pods/" options.resourcePrefix = prefix options.keyFunc = func(obj runtime.Object) (string, error) { return storage.NamespaceKeyFunc(prefix, obj) } @@ -563,9 +569,15 @@ func testSetupWithEtcdServer(t testing.TB, opts ...setupOption) (context.Context server, etcdStorage := newEtcdTestStorage(t, etcd3testing.PathPrefix()) // Inject one list error to make sure we test the relist case. + listErrors := 1 + if clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) { + // The WatchListClient feature changes the reflector to use WATCH + // instead of LIST, therefore we don't expect any errors + listErrors = 0 + } wrappedStorage := &storagetesting.StorageInjectingListErrors{ Interface: etcdStorage, - Errors: 1, + Errors: listErrors, } config := Config{ diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_testing_utils_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_testing_utils_test.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_testing_utils_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_testing_utils_test.go index 8c08f777d..245e9a0b9 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_testing_utils_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_testing_utils_test.go @@ -63,19 +63,22 @@ func newEtcdTestStorage(t testing.TB, prefix string) (*etcd3testing.EtcdTestServ codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion) compactor := etcd3.NewCompactor(server.V3Client.Client, 0, clock.RealClock{}, nil) t.Cleanup(compactor.Stop) - storage := etcd3.New( + storage, err := etcd3.New( server.V3Client, compactor, codec, newPod, newPodList, prefix, - "/pods", + "/pods/", schema.GroupResource{Resource: "pods"}, identity.NewEncryptCheckTransformer(), etcd3.NewDefaultLeaseManagerConfig(), etcd3.NewDefaultDecoder(codec, versioner), versioner) + if err != nil { + t.Fatal(err) + } t.Cleanup(storage.Close) return server, storage } @@ -162,10 +165,12 @@ func compactStore(c *CacheDelegator, client *clientv3.Client) storagetesting.Com } } -func increaseRV(client *clientv3.Client) storagetesting.IncreaseRVFunc { - return func(ctx context.Context, t *testing.T) { - if _, err := client.KV.Put(ctx, "increaseRV", "ok"); err != nil { +func increaseRVFunc(client *clientv3.Client) storagetesting.IncreaseRVFunc { + return func(ctx context.Context, t *testing.T) int64 { + resp, err := client.KV.Put(ctx, "increaseRV", "ok") + if err != nil { t.Fatalf("Could not update increaseRV: %v", err) } + return resp.Header.Revision } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_whitebox_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_whitebox_test.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_whitebox_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_whitebox_test.go index bd3ce3237..8005bf1ae 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/cacher_whitebox_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/cacher_whitebox_test.go @@ -30,18 +30,19 @@ import ( "time" "github.com/stretchr/testify/require" - "google.golang.org/grpc/metadata" - "k8s.io/apimachinery/pkg/apis/meta/internalversion" - "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/sharding" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" @@ -51,20 +52,25 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/cacher/delegator" "k8s.io/apiserver/pkg/storage/cacher/metrics" + "k8s.io/apiserver/pkg/storage/cacher/store" etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" etcdfeature "k8s.io/apiserver/pkg/storage/feature" storagetesting "k8s.io/apiserver/pkg/storage/testing" utilfeature "k8s.io/apiserver/pkg/util/feature" + clientfeatures "k8s.io/client-go/features" + clientfeaturestesting "k8s.io/client-go/features/testing" featuregatetesting "k8s.io/component-base/featuregate/testing" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/testutil" "k8s.io/utils/clock" testingclock "k8s.io/utils/clock/testing" "k8s.io/utils/ptr" + + cachertesting "k8s.io/apiserver/pkg/storage/cacher/testing" ) func newTestCacherWithoutSyncing(s storage.Interface, c clock.WithTicker) (*Cacher, storage.Versioner, error) { - prefix := "pods" + prefix := "/pods/" config := Config{ Storage: s, Versioner: storage.APIObjectVersioner{}, @@ -111,125 +117,6 @@ func newTestCacher(s storage.Interface) (*Cacher, storage.Versioner, error) { return cacher, versioner, nil } -type dummyStorage struct { - sync.RWMutex - getlistErr error - watchErr error - getListFn func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error - getRVFn func(_ context.Context) (uint64, error) - watchFn func(_ context.Context, _ string, _ storage.ListOptions) (watch.Interface, error) - - // use getRequestWatchProgressCounter when reading - // the value of the counter - requestWatchProgressCounter int -} - -func (d *dummyStorage) RequestWatchProgress(ctx context.Context) error { - d.Lock() - defer d.Unlock() - d.requestWatchProgressCounter++ - return nil -} - -func (d *dummyStorage) getRequestWatchProgressCounter() int { - d.RLock() - defer d.RUnlock() - return d.requestWatchProgressCounter -} - -func (d *dummyStorage) CompactRevision() int64 { - return 0 -} - -type dummyWatch struct { - ch chan watch.Event -} - -func (w *dummyWatch) ResultChan() <-chan watch.Event { - return w.ch -} - -func (w *dummyWatch) Stop() { - close(w.ch) -} - -func newDummyWatch() watch.Interface { - return &dummyWatch{ - ch: make(chan watch.Event), - } -} - -func (d *dummyStorage) Versioner() storage.Versioner { return nil } -func (d *dummyStorage) Create(_ context.Context, _ string, _, _ runtime.Object, _ uint64) error { - return fmt.Errorf("unimplemented") -} -func (d *dummyStorage) Delete(_ context.Context, _ string, _ runtime.Object, _ *storage.Preconditions, _ storage.ValidateObjectFunc, _ runtime.Object, _ storage.DeleteOptions) error { - return fmt.Errorf("unimplemented") -} -func (d *dummyStorage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { - if d.watchFn != nil { - return d.watchFn(ctx, key, opts) - } - d.RLock() - defer d.RUnlock() - - return newDummyWatch(), d.watchErr -} -func (d *dummyStorage) Get(_ context.Context, _ string, _ storage.GetOptions, _ runtime.Object) error { - d.RLock() - defer d.RUnlock() - return d.getlistErr -} -func (d *dummyStorage) GetList(ctx context.Context, resPrefix string, opts storage.ListOptions, listObj runtime.Object) error { - if d.getListFn != nil { - return d.getListFn(ctx, resPrefix, opts, listObj) - } - d.RLock() - defer d.RUnlock() - podList := listObj.(*example.PodList) - podList.ListMeta = metav1.ListMeta{ResourceVersion: "100"} - return d.getlistErr -} -func (d *dummyStorage) GuaranteedUpdate(_ context.Context, _ string, _ runtime.Object, _ bool, _ *storage.Preconditions, _ storage.UpdateFunc, _ runtime.Object) error { - return fmt.Errorf("unimplemented") -} -func (d *dummyStorage) Stats(_ context.Context) (storage.Stats, error) { - return storage.Stats{}, fmt.Errorf("unimplemented") -} -func (d *dummyStorage) SetKeysFunc(storage.KeysFunc) { -} -func (d *dummyStorage) ReadinessCheck() error { - return nil -} -func (d *dummyStorage) injectGetListError(err error) { - d.Lock() - defer d.Unlock() - - d.getlistErr = err -} - -func (d *dummyStorage) GetCurrentResourceVersion(ctx context.Context) (uint64, error) { - if d.getRVFn != nil { - return d.getRVFn(ctx) - } - return 100, nil -} - -type dummyCacher struct { - dummyStorage - ready bool - - consistent bool -} - -func (d *dummyCacher) Ready() bool { - return d.ready -} - -func (d *dummyCacher) MarkConsistent(consistent bool) { - d.consistent = consistent -} - func TestShouldDelegateList(t *testing.T) { type opts struct { Recursive bool @@ -365,8 +252,8 @@ func TestShouldDelegateList(t *testing.T) { expectBypass = bypass } } - backingStorage := &dummyStorage{} - backingStorage.getListFn = func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error { + backingStorage := &cachertesting.MockStorage{} + backingStorage.GetListFn = func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error { podList := listObj.(*example.PodList) podList.ListMeta = metav1.ListMeta{ResourceVersion: cacheRV} return nil @@ -561,8 +448,8 @@ apiserver_watch_cache_consistent_read_total{fallback="true", group="", resource= if err := registry.Register(metrics.ConsistentReadTotal); err != nil { t.Errorf("unexpected error: %v", err) } - backingStorage := &dummyStorage{} - backingStorage.getListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + backingStorage := &cachertesting.MockStorage{} + backingStorage.GetListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { podList := listObj.(*example.PodList) podList.ResourceVersion = tc.watchCacheRV return nil @@ -583,7 +470,7 @@ apiserver_watch_cache_consistent_read_total{fallback="true", group="", resource= t.Fatalf("Expected watch cache RV to equal watchCacheRV, got: %d, want: %s", cacher.watchCache.resourceVersion, tc.watchCacheRV) } requestToStorageCount := 0 - backingStorage.getListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + backingStorage.GetListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { requestToStorageCount += 1 podList := listObj.(*example.PodList) if key == cacher.resourcePrefix { @@ -596,7 +483,7 @@ apiserver_watch_cache_consistent_read_total{fallback="true", group="", resource= podList.ResourceVersion = tc.storageRV return nil } - backingStorage.getRVFn = func(_ context.Context) (uint64, error) { + backingStorage.GetRVFn = func(_ context.Context) (uint64, error) { requestToStorageCount += 1 rv, err := strconv.Atoi(tc.storageRV) if err != nil { @@ -625,7 +512,7 @@ apiserver_watch_cache_consistent_read_total{fallback="true", group="", resource= } start := cacher.clock.Now() - err = delegator.GetList(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "", Recursive: true}, result) + err = delegator.GetList(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "", Recursive: true}, result) clockStepCancelFn() duration := cacher.clock.Since(start) if (err != nil) != tc.expectError { @@ -679,9 +566,9 @@ func TestMatchExactResourceVersionFallback(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ListFromCacheSnapshot, true) - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} expectStoreRequests := 0 - backingStorage.getListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + backingStorage.GetListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { expectStoreRequests++ podList := listObj.(*example.PodList) switch opts.ResourceVersionMatch { @@ -700,7 +587,7 @@ func TestMatchExactResourceVersionFallback(t *testing.T) { snapshotRequestCount := 0 cacher.watchCache.RWMutex.Lock() cacher.watchCache.snapshots = &fakeSnapshotter{ - getLessOrEqual: func(rv uint64) (orderedLister, bool) { + getLessOrEqual: func(rv uint64) (store.OrderedLister, bool) { snapshotAvailable := tc.snapshotsAvailable[snapshotRequestCount] snapshotRequestCount++ if snapshotAvailable { @@ -717,7 +604,7 @@ func TestMatchExactResourceVersionFallback(t *testing.T) { delegator := NewCacheDelegator(cacher, backingStorage) result := &example.PodList{} - err = delegator.GetList(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "20", ResourceVersionMatch: metav1.ResourceVersionMatchExact, Recursive: true}, result) + err = delegator.GetList(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "20", ResourceVersionMatch: metav1.ResourceVersionMatchExact, Recursive: true}, result) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -738,7 +625,7 @@ func TestMatchExactResourceVersionFallback(t *testing.T) { func TestGetListNonRecursiveCacheBypass(t *testing.T) { featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.33")) featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentListFromCache, false) - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -759,8 +646,8 @@ func TestGetListNonRecursiveCacheBypass(t *testing.T) { } // Inject error to underlying layer and check if cacher is not bypassed. - backingStorage.injectGetListError(errDummy) - err = delegator.GetList(context.TODO(), "pods/ns", storage.ListOptions{ + backingStorage.InjectGetListError(errDummy) + err = delegator.GetList(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "0", Predicate: pred, }, result) @@ -768,7 +655,7 @@ func TestGetListNonRecursiveCacheBypass(t *testing.T) { t.Errorf("GetList with Limit and RV=0 should be served from cache: %v", err) } - err = delegator.GetList(context.TODO(), "pods/ns", storage.ListOptions{ + err = delegator.GetList(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "", Predicate: pred, }, result) @@ -779,8 +666,10 @@ func TestGetListNonRecursiveCacheBypass(t *testing.T) { func TestGetListNonRecursiveCacheWithConsistentListFromCache(t *testing.T) { // Set feature gates once at the beginning since we only care about ConsistentListFromCache=true and ListFromCacheSnapshot=false - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentListFromCache, true) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ListFromCacheSnapshot, false) + featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{ + features.ConsistentListFromCache: true, + features.ListFromCacheSnapshot: false, + }) forceRequestWatchProgressSupport(t) tests := []struct { @@ -804,9 +693,9 @@ func TestGetListNonRecursiveCacheWithConsistentListFromCache(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var getListCount, getCurrentRVCount int - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} - backingStorage.getListFn = func(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + backingStorage.GetListFn = func(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { getListCount++ if tc.injectRVError { return errDummy @@ -816,7 +705,7 @@ func TestGetListNonRecursiveCacheWithConsistentListFromCache(t *testing.T) { return nil } - backingStorage.getRVFn = func(ctx context.Context) (uint64, error) { + backingStorage.GetRVFn = func(ctx context.Context) (uint64, error) { getCurrentRVCount++ rv := uint64(100) err := error(nil) @@ -841,7 +730,7 @@ func TestGetListNonRecursiveCacheWithConsistentListFromCache(t *testing.T) { defer delegator.Stop() // Setup test object - key := "pods/ns" + key := "/pods/ns" input := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}} if err := v.UpdateObject(input, 100); err != nil { t.Fatalf("Unexpected error: %v", err) @@ -897,7 +786,7 @@ func TestGetListNonRecursiveCacheWithConsistentListFromCache(t *testing.T) { } } func TestGetCacheBypass(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -915,8 +804,8 @@ func TestGetCacheBypass(t *testing.T) { } // Inject error to underlying layer and check if cacher is not bypassed. - backingStorage.injectGetListError(errDummy) - err = delegator.Get(context.TODO(), "pods/ns/pod-0", storage.GetOptions{ + backingStorage.InjectGetListError(errDummy) + err = delegator.Get(context.TODO(), "/pods/ns/pod-0", storage.GetOptions{ IgnoreNotFound: true, ResourceVersion: "0", }, result) @@ -924,7 +813,7 @@ func TestGetCacheBypass(t *testing.T) { t.Errorf("Get with RV=0 should be served from cache: %v", err) } - err = delegator.Get(context.TODO(), "pods/ns/pod-0", storage.GetOptions{ + err = delegator.Get(context.TODO(), "/pods/ns/pod-0", storage.GetOptions{ IgnoreNotFound: true, ResourceVersion: "", }, result) @@ -934,7 +823,7 @@ func TestGetCacheBypass(t *testing.T) { } func TestWatchCacheBypass(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -949,7 +838,7 @@ func TestWatchCacheBypass(t *testing.T) { } } - _, err = delegator.Watch(context.TODO(), "pod/ns", storage.ListOptions{ + _, err = delegator.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "0", Predicate: storage.Everything, }) @@ -957,7 +846,7 @@ func TestWatchCacheBypass(t *testing.T) { t.Errorf("Watch with RV=0 should be served from cache: %v", err) } - _, err = delegator.Watch(context.TODO(), "pod/ns", storage.ListOptions{ + _, err = delegator.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "", Predicate: storage.Everything, }) @@ -973,7 +862,7 @@ func TestTooManyRequestsNotReturned(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ResilientWatchCacheInitialization, false) dummyErr := fmt.Errorf("dummy") - backingStorage := &dummyStorage{getlistErr: dummyErr} + backingStorage := &cachertesting.MockStorage{GetListErr: dummyErr} cacher, _, err := newTestCacherWithoutSyncing(backingStorage, clock.RealClock{}) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1101,7 +990,7 @@ func TestWatchNotHangingOnStartupFailure(t *testing.T) { // Configure cacher so that it can't initialize, because of // constantly failing lists to the underlying storage. dummyErr := fmt.Errorf("dummy") - backingStorage := &dummyStorage{watchErr: dummyErr} + backingStorage := &cachertesting.MockStorage{WatchErr: dummyErr} cacher, _, err := newTestCacherWithoutSyncing(backingStorage, testingclock.NewFakeClock(time.Now())) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1119,7 +1008,7 @@ func TestWatchNotHangingOnStartupFailure(t *testing.T) { // Watch hangs waiting on watchcache being initialized. // Ensure that it terminates when its context is cancelled // (e.g. the request is terminated for whatever reason). - _, err = cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "0"}) + _, err = cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "0"}) if !utilfeature.DefaultFeatureGate.Enabled(features.ResilientWatchCacheInitialization) { if err == nil || err.Error() != apierrors.NewServiceUnavailable(context.Canceled.Error()).Error() { t.Errorf("Unexpected error: %#v", err) @@ -1132,7 +1021,7 @@ func TestWatchNotHangingOnStartupFailure(t *testing.T) { } func TestWatcherNotGoingBackInTime(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, v, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1164,7 +1053,7 @@ func TestWatcherNotGoingBackInTime(t *testing.T) { totalPods := 100 // Create watcher that will be slowing down reading. - w1, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ + w1, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "999", Predicate: storage.Everything, }) @@ -1189,7 +1078,7 @@ func TestWatcherNotGoingBackInTime(t *testing.T) { } // Create fast watcher and ensure it will get each object exactly once. - w2, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) + w2, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1220,7 +1109,7 @@ func TestWatcherNotGoingBackInTime(t *testing.T) { } func TestCacherDontAcceptRequestsStopped(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1234,7 +1123,7 @@ func TestCacherDontAcceptRequestsStopped(t *testing.T) { } } - w, err := delegator.Watch(context.Background(), "pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) + w, err := delegator.Watch(context.Background(), "/pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1254,13 +1143,13 @@ func TestCacherDontAcceptRequestsStopped(t *testing.T) { cacher.Stop() - _, err = delegator.Watch(context.Background(), "pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) + _, err = delegator.Watch(context.Background(), "/pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything}) if err == nil { t.Fatalf("Success to create Watch: %v", err) } result := &example.Pod{} - err = delegator.Get(context.TODO(), "pods/ns/pod-0", storage.GetOptions{ + err = delegator.Get(context.TODO(), "/pods/ns/pod-0", storage.GetOptions{ IgnoreNotFound: true, ResourceVersion: "1", }, result) @@ -1275,7 +1164,7 @@ func TestCacherDontAcceptRequestsStopped(t *testing.T) { } listResult := &example.PodList{} - err = delegator.GetList(context.TODO(), "pods/ns", storage.ListOptions{ + err = delegator.GetList(context.TODO(), "/pods/ns", storage.ListOptions{ ResourceVersion: "1", Recursive: true, Predicate: storage.SelectionPredicate{ @@ -1311,8 +1200,8 @@ func TestCacherDontMissEventsOnReinitialization(t *testing.T) { } listCalls, watchCalls := 0, 0 - backingStorage := &dummyStorage{ - getListFn: func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error { + backingStorage := &cachertesting.MockStorage{ + GetListFn: func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error { podList := listObj.(*example.PodList) var err error switch listCalls { @@ -1327,7 +1216,7 @@ func TestCacherDontMissEventsOnReinitialization(t *testing.T) { listCalls++ return err }, - watchFn: func(_ context.Context, _ string, _ storage.ListOptions) (watch.Interface, error) { + WatchFn: func(_ context.Context, _ string, _ storage.ListOptions) (watch.Interface, error) { var w *watch.FakeWatcher var err error switch watchCalls { @@ -1373,7 +1262,7 @@ func TestCacherDontMissEventsOnReinitialization(t *testing.T) { for i := 0; i < concurrency; i++ { go func() { defer wg.Done() - w, err := cacher.Watch(ctx, "pods", storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything}) + w, err := cacher.Watch(ctx, "/pods/", storage.ListOptions{ResourceVersion: "1", Predicate: storage.Everything}) if err != nil { // Watch failed to initialize (this most probably means that cacher // already moved back to Pending state before watch initialized. @@ -1409,7 +1298,7 @@ func TestCacherDontMissEventsOnReinitialization(t *testing.T) { } func TestCacherNoLeakWithMultipleWatchers(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1442,7 +1331,7 @@ func TestCacherNoLeakWithMultipleWatchers(t *testing.T) { default: ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: pred}) + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: pred}) if err != nil { watchErr = fmt.Errorf("Failed to create watch: %v", err) return @@ -1490,7 +1379,7 @@ func TestCacherNoLeakWithMultipleWatchers(t *testing.T) { } func testCacherSendBookmarkEvents(t *testing.T, allowWatchBookmarks, expectedBookmarks bool) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1507,7 +1396,7 @@ func testCacherSendBookmarkEvents(t *testing.T, allowWatchBookmarks, expectedBoo ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: pred}) + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "0", Predicate: pred}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1590,7 +1479,7 @@ func TestInitialEventsEndBookmark(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, true) forceRequestWatchProgressSupport(t) - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1676,7 +1565,7 @@ func TestInitialEventsEndBookmark(t *testing.T) { pred.AllowWatchBookmarks = scenario.allowWatchBookmarks ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "100", SendInitialEvents: scenario.sendInitialEvents, Predicate: pred}) + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "100", SendInitialEvents: scenario.sendInitialEvents, Predicate: pred}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1687,7 +1576,7 @@ func TestInitialEventsEndBookmark(t *testing.T) { } func TestCacherSendsMultipleWatchBookmarks(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1723,7 +1612,7 @@ func TestCacherSendsMultipleWatchBookmarks(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "100", Predicate: pred}) + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "100", Predicate: pred}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1762,7 +1651,7 @@ func TestCacherSendsMultipleWatchBookmarks(t *testing.T) { } func TestDispatchingBookmarkEventsWithConcurrentStop(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1794,7 +1683,7 @@ func TestDispatchingBookmarkEventsWithConcurrentStop(t *testing.T) { pred.AllowWatchBookmarks = true ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: pred}) + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: pred}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -1838,7 +1727,7 @@ func TestDispatchingBookmarkEventsWithConcurrentStop(t *testing.T) { } func TestBookmarksOnResourceVersionUpdates(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1920,7 +1809,7 @@ func (f *fakeTimeBudget) takeAvailable() time.Duration { func (f *fakeTimeBudget) returnUnused(_ time.Duration) {} func TestStartingResourceVersion(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -1968,7 +1857,7 @@ func TestStartingResourceVersion(t *testing.T) { // Advance RV by 10. startVersion := uint64(1010) - watcher, err := cacher.Watch(context.TODO(), "pods/ns/foo", storage.ListOptions{ResourceVersion: strconv.FormatUint(startVersion, 10), Predicate: storage.Everything}) + watcher, err := cacher.Watch(context.TODO(), "/pods/ns/foo", storage.ListOptions{ResourceVersion: strconv.FormatUint(startVersion, 10), Predicate: storage.Everything}) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -2001,7 +1890,7 @@ func TestStartingResourceVersion(t *testing.T) { } func TestDispatchEventWillNotBeBlockedByTimedOutWatcher(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -2045,14 +1934,14 @@ func TestDispatchEventWillNotBeBlockedByTimedOutWatcher(t *testing.T) { totalPods := 50 // Create watcher that will be blocked. - w1, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) + w1, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } defer w1.Stop() // Create fast watcher and ensure it will get all objects. - w2, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) + w2, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -2144,7 +2033,7 @@ func verifyEvents(t *testing.T, w watch.Interface, events []watch.Event, strictO } func TestCachingDeleteEvents(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -2167,7 +2056,7 @@ func TestCachingDeleteEvents(t *testing.T) { } createWatch := func(pred storage.SelectionPredicate) watch.Interface { - w, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: pred}) + w, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "999", Predicate: pred}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -2227,7 +2116,7 @@ func TestCachingDeleteEvents(t *testing.T) { } func testCachingObjects(t *testing.T, watchersCount int) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -2248,7 +2137,7 @@ func testCachingObjects(t *testing.T, watchersCount int) { watchers := make([]watch.Interface, 0, watchersCount) for i := 0; i < watchersCount; i++ { - w, err := cacher.Watch(context.TODO(), "pods/ns", storage.ListOptions{ResourceVersion: "1000", Predicate: storage.Everything}) + w, err := cacher.Watch(context.TODO(), "/pods/ns", storage.ListOptions{ResourceVersion: "1000", Predicate: storage.Everything}) if err != nil { t.Fatalf("Failed to create watch: %v", err) } @@ -2324,7 +2213,7 @@ func TestCachingObjects(t *testing.T) { } func TestCacheIntervalInvalidationStopsWatch(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) if err != nil { t.Fatalf("Couldn't create cacher: %v", err) @@ -2384,7 +2273,7 @@ func TestCacheIntervalInvalidationStopsWatch(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - w, err := cacher.Watch(ctx, "pods/ns", storage.ListOptions{ + w, err := cacher.Watch(ctx, "/pods/ns", storage.ListOptions{ ResourceVersion: "999", Predicate: storage.Everything, }) @@ -2410,15 +2299,17 @@ func TestCacheIntervalInvalidationStopsWatch(t *testing.T) { } func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, true) - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ConsistentListFromCache, true) + featuregatetesting.SetFeatureGatesDuringTest(t, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{ + features.WatchList: true, + features.ConsistentListFromCache: true, + }) forceRequestWatchProgressSupport(t) scenarios := []struct { name string opts storage.ListOptions - backingStorage *dummyStorage - verifyBackingStore func(t *testing.T, s *dummyStorage) + backingStorage *cachertesting.MockStorage + verifyBackingStore func(t *testing.T, s *cachertesting.MockStorage) }{ { name: "allowWatchBookmarks=true, sendInitialEvents=true, RV=105", @@ -2431,8 +2322,8 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { SendInitialEvents: ptr.To(true), ResourceVersion: "105", }, - verifyBackingStore: func(t *testing.T, s *dummyStorage) { - require.NotEqual(t, 0, s.getRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") + verifyBackingStore: func(t *testing.T, s *cachertesting.MockStorage) { + require.NotEqual(t, 0, s.GetRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") }, }, @@ -2446,10 +2337,10 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { }(), SendInitialEvents: ptr.To(true), }, - backingStorage: func() *dummyStorage { + backingStorage: func() *cachertesting.MockStorage { hasBeenPrimed := false - s := &dummyStorage{} - s.getListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + s := &cachertesting.MockStorage{} + s.GetListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { listAccessor, err := meta.ListAccessor(listObj) if err != nil { return err @@ -2464,7 +2355,7 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { listAccessor.SetResourceVersion("105") return nil } - s.getRVFn = func(_ context.Context) (uint64, error) { + s.GetRVFn = func(_ context.Context) (uint64, error) { // the first call to this function // primes the cacher if !hasBeenPrimed { @@ -2475,8 +2366,8 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { } return s }(), - verifyBackingStore: func(t *testing.T, s *dummyStorage) { - require.NotEqual(t, 0, s.getRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") + verifyBackingStore: func(t *testing.T, s *cachertesting.MockStorage) { + require.NotEqual(t, 0, s.GetRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") }, }, @@ -2490,10 +2381,10 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { }(), SendInitialEvents: ptr.To(true), }, - backingStorage: func() *dummyStorage { + backingStorage: func() *cachertesting.MockStorage { hasBeenPrimed := false - s := &dummyStorage{} - s.getListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + s := &cachertesting.MockStorage{} + s.GetListFn = func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { listAccessor, err := meta.ListAccessor(listObj) if err != nil { return err @@ -2508,7 +2399,7 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { listAccessor.SetResourceVersion("105") return nil } - s.getRVFn = func(_ context.Context) (uint64, error) { + s.GetRVFn = func(_ context.Context) (uint64, error) { // the first call to this function // primes the cacher if !hasBeenPrimed { @@ -2519,8 +2410,8 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { } return s }(), - verifyBackingStore: func(t *testing.T, s *dummyStorage) { - require.NotEqual(t, 0, s.getRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") + verifyBackingStore: func(t *testing.T, s *cachertesting.MockStorage) { + require.NotEqual(t, 0, s.GetRequestWatchProgressCounter(), "expected store.RequestWatchProgressCounter to be > 0. It looks like watch progress wasn't requested!") }, }, } @@ -2528,11 +2419,11 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { t.Parallel() - var backingStorage *dummyStorage + var backingStorage *cachertesting.MockStorage if scenario.backingStorage != nil { backingStorage = scenario.backingStorage } else { - backingStorage = &dummyStorage{} + backingStorage = &cachertesting.MockStorage{} } cacher, _, err := newTestCacher(backingStorage) if err != nil { @@ -2546,7 +2437,7 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { } } - w, err := cacher.Watch(context.Background(), "pods/ns", scenario.opts) + w, err := cacher.Watch(context.Background(), "/pods/ns", scenario.opts) require.NoError(t, err, "failed to create watch: %v") defer w.Stop() var expectedErr *apierrors.StatusError @@ -2572,7 +2463,7 @@ func TestWaitUntilWatchCacheFreshAndForceAllEvents(t *testing.T) { t.Errorf("failed adding a pod to the watchCache %v", err) } }(t) - w, err = cacher.Watch(context.Background(), "pods/ns", scenario.opts) + w, err = cacher.Watch(context.Background(), "/pods/ns", scenario.opts) require.NoError(t, err, "failed to create watch: %v") defer w.Stop() verifyEvents(t, w, []watch.Event{ @@ -2606,7 +2497,7 @@ func (m fakeStorage) GetList(ctx context.Context, key string, opts storage.ListO return nil } func (m fakeStorage) Watch(_ context.Context, _ string, _ storage.ListOptions) (watch.Interface, error) { - return newDummyWatch(), nil + return cachertesting.NewMockWatch(), nil } func BenchmarkCacher_GetList(b *testing.B) { @@ -2679,7 +2570,7 @@ func BenchmarkCacher_GetList(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { result := &example.PodList{} - err = delegator.GetList(context.TODO(), "pods", storage.ListOptions{ + err = delegator.GetList(context.TODO(), "/pods/", storage.ListOptions{ Predicate: pred, Recursive: true, ResourceVersion: "12345", @@ -2699,7 +2590,7 @@ func BenchmarkCacher_GetList(b *testing.B) { // a bookmark event will be delivered even if the cacher has not received an event. func TestWatchListIsSynchronisedWhenNoEventsFromStoreReceived(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, true) - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) require.NoError(t, err, "failed to create cacher") defer cacher.Stop() @@ -2716,7 +2607,7 @@ func TestWatchListIsSynchronisedWhenNoEventsFromStoreReceived(t *testing.T) { Predicate: pred, SendInitialEvents: ptr.To(true), } - w, err := cacher.Watch(context.Background(), "pods/ns", opts) + w, err := cacher.Watch(context.Background(), "/pods/ns", opts) require.NoError(t, err, "failed to create watch: %v") defer w.Stop() @@ -2731,7 +2622,7 @@ func TestWatchListIsSynchronisedWhenNoEventsFromStoreReceived(t *testing.T) { } func TestForgetWatcher(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) require.NoError(t, err) defer cacher.Stop() @@ -2759,7 +2650,7 @@ func TestForgetWatcher(t *testing.T) { } w := newCacheWatcher( 0, - func(_ string, _ labels.Set, _ fields.Set) bool { return true }, + func(_ string, _ labels.Set, _ fields.Set, _ runtime.Object) bool { return true }, nil, storage.APIObjectVersioner{}, testingclock.NewFakeClock(time.Now()).Now().Add(2*time.Minute), @@ -2930,7 +2821,7 @@ func TestGetWatchCacheResourceVersion(t *testing.T) { for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) require.NoError(t, err, "couldn't create cacher") defer cacher.Stop() @@ -3124,7 +3015,7 @@ func TestGetBookmarkAfterResourceVersionLockedFunc(t *testing.T) { } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} cacher, _, err := newTestCacher(backingStorage) require.NoError(t, err, "couldn't create cacher") @@ -3153,122 +3044,6 @@ func TestGetBookmarkAfterResourceVersionLockedFunc(t *testing.T) { } } -func TestWatchStreamSeparation(t *testing.T) { - server, etcdStorage := newEtcdTestStorage(t, etcd3testing.PathPrefix()) - t.Cleanup(func() { - server.Terminate(t) - }) - setupOpts := &setupOptions{} - withDefaults(setupOpts) - config := Config{ - Storage: etcdStorage, - Versioner: storage.APIObjectVersioner{}, - GroupResource: schema.GroupResource{Resource: "pods"}, - EventsHistoryWindow: DefaultEventFreshDuration, - ResourcePrefix: setupOpts.resourcePrefix, - KeyFunc: setupOpts.keyFunc, - GetAttrsFunc: GetPodAttrs, - NewFunc: newPod, - NewListFunc: newPodList, - IndexerFuncs: setupOpts.indexerFuncs, - Codec: codecs.LegacyCodec(examplev1.SchemeGroupVersion), - Clock: setupOpts.clock, - } - tcs := []struct { - name string - separateCacheWatchRPC bool - useWatchCacheContextMetadata bool - expectBookmarkOnWatchCache bool - expectBookmarkOnEtcd bool - }{ - { - name: "common RPC > both get bookmarks", - separateCacheWatchRPC: false, - expectBookmarkOnEtcd: true, - expectBookmarkOnWatchCache: true, - }, - { - name: "separate RPC > only etcd gets bookmarks", - separateCacheWatchRPC: true, - expectBookmarkOnEtcd: true, - expectBookmarkOnWatchCache: false, - }, - { - name: "separate RPC & watch cache context > only watch cache gets bookmarks", - separateCacheWatchRPC: true, - useWatchCacheContextMetadata: true, - expectBookmarkOnEtcd: false, - expectBookmarkOnWatchCache: true, - }, - } - for _, tc := range tcs { - t.Run(tc.name, func(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SeparateCacheWatchRPC, tc.separateCacheWatchRPC) - cacher, err := NewCacherFromConfig(config) - if err != nil { - t.Fatalf("Failed to initialize cacher: %v", err) - } - defer cacher.Stop() - - if !utilfeature.DefaultFeatureGate.Enabled(features.ResilientWatchCacheInitialization) { - if err := cacher.ready.wait(context.TODO()); err != nil { - t.Fatalf("unexpected error waiting for the cache to be ready") - } - } - - getCacherRV := func() uint64 { - cacher.watchCache.RLock() - defer cacher.watchCache.RUnlock() - return cacher.watchCache.resourceVersion - } - waitContext, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - waitForEtcdBookmark := watchAndWaitForBookmark(t, waitContext, cacher.storage) - - var out example.Pod - err = cacher.storage.Create(context.Background(), "foo", &example.Pod{}, &out, 0) - if err != nil { - t.Fatal(err) - } - err = cacher.storage.Delete(context.Background(), "foo", &out, nil, storage.ValidateAllObjectFunc, &example.Pod{}, storage.DeleteOptions{}) - if err != nil { - t.Fatal(err) - } - versioner := storage.APIObjectVersioner{} - var lastResourceVersion uint64 - lastResourceVersion, err = versioner.ObjectResourceVersion(&out) - if err != nil { - t.Fatal(err) - } - - var contextMetadata metadata.MD - if tc.useWatchCacheContextMetadata { - contextMetadata = metadata.New(map[string]string{"source": "cache"}) - } - // For the first 100ms from watch creation, watch progress requests are ignored. - time.Sleep(200 * time.Millisecond) - err = cacher.storage.RequestWatchProgress(metadata.NewOutgoingContext(context.Background(), contextMetadata)) - if err != nil { - t.Fatal(err) - } - // Give time for bookmark to arrive - time.Sleep(time.Second) - - etcdWatchResourceVersion := waitForEtcdBookmark() - gotEtcdWatchBookmark := etcdWatchResourceVersion == lastResourceVersion - if gotEtcdWatchBookmark != tc.expectBookmarkOnEtcd { - t.Errorf("Unexpected etcd bookmark check result, rv: %d, lastRV: %d, wantMatching: %v", etcdWatchResourceVersion, lastResourceVersion, tc.expectBookmarkOnEtcd) - } - - watchCacheResourceVersion := getCacherRV() - cacherGotBookmark := watchCacheResourceVersion == lastResourceVersion - if cacherGotBookmark != tc.expectBookmarkOnWatchCache { - t.Errorf("Unexpected watch cache bookmark check result, rv: %d, lastRV: %d, wantMatching: %v", watchCacheResourceVersion, lastResourceVersion, tc.expectBookmarkOnWatchCache) - } - }) - } -} - func TestComputeListLimit(t *testing.T) { scenarios := []struct { name string @@ -3326,37 +3101,6 @@ func TestComputeListLimit(t *testing.T) { } } -func watchAndWaitForBookmark(t *testing.T, ctx context.Context, etcdStorage storage.Interface) func() (resourceVersion uint64) { - opts := storage.ListOptions{ResourceVersion: "", Predicate: storage.Everything, Recursive: true} - opts.Predicate.AllowWatchBookmarks = true - w, err := etcdStorage.Watch(ctx, "/pods/", opts) - if err != nil { - t.Fatal(err) - } - - versioner := storage.APIObjectVersioner{} - var rv uint64 - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - for event := range w.ResultChan() { - if event.Type == watch.Bookmark { - rv, err = versioner.ObjectResourceVersion(event.Object) - break - } - } - }() - return func() (resourceVersion uint64) { - defer w.Stop() - wg.Wait() - if err != nil { - t.Fatal(err) - } - return rv - } -} - // TODO(p0lyn0mial): forceRequestWatchProgressSupport inits the storage layer // so that tests that require storage.RequestWatchProgress pass // @@ -3526,7 +3270,7 @@ func TestRetryAfterForUnreadyCache(t *testing.T) { if !utilfeature.DefaultFeatureGate.Enabled(features.ResilientWatchCacheInitialization) { t.Skipf("the test requires %v to be enabled", features.ResilientWatchCacheInitialization) } - backingStorage := &dummyStorage{} + backingStorage := &cachertesting.MockStorage{} clock := testingclock.NewFakeClock(time.Now()) cacher, _, err := newTestCacherWithoutSyncing(backingStorage, clock) if err != nil { @@ -3563,3 +3307,288 @@ func TestRetryAfterForUnreadyCache(t *testing.T) { t.Fatalf("Unexpected retry after: %v", statusError.Status().Details.RetryAfterSeconds) } } + +func TestWatchListSemanticsSimple(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, true) + clientfeaturestesting.SetFeatureDuringTest(t, clientfeatures.WatchListClient, true) + + // The dummyStore doesn’t support WatchList semantics, + // so we don’t need to prepare a response. + backingStorage := &cachertesting.MockStorage{} + clock := testingclock.NewFakeClock(time.Now()) + cacher, _, err := newTestCacherWithoutSyncing(backingStorage, clock) + if err != nil { + t.Fatalf("couldn't create cacher: %v", err) + } + defer cacher.Stop() + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second) + defer cancel() + if err = cacher.ready.wait(ctx); err != nil { + t.Fatalf("error waiting for the cache to be ready, err: %v", err) + } +} + +type fakeOrderedLister struct { +} + +func (f fakeOrderedLister) Add(obj interface{}) error { return nil } +func (f fakeOrderedLister) Update(obj interface{}) error { return nil } +func (f fakeOrderedLister) Delete(obj interface{}) error { return nil } +func (f fakeOrderedLister) Clone() store.OrderedLister { return f } +func (f fakeOrderedLister) ListPrefix(prefixKey, continueKey string) []interface{} { + return nil +} +func (f fakeOrderedLister) Count(prefixKey, continueKey string) int { return 0 } + +type fakeSnapshotter struct { + getLessOrEqual func(rv uint64) (store.OrderedLister, bool) +} + +var _ store.Snapshotter = (*fakeSnapshotter)(nil) + +func (f *fakeSnapshotter) Reset() {} +func (f *fakeSnapshotter) GetLessOrEqual(rv uint64) (store.OrderedLister, bool) { + if f.getLessOrEqual == nil { + return nil, false + } + return f.getLessOrEqual(rv) +} +func (f *fakeSnapshotter) Add(rv uint64, indexer store.OrderedLister) {} +func (f *fakeSnapshotter) RemoveLess(rv uint64) {} +func (f *fakeSnapshotter) Len() int { + return 0 +} + +// --- Sharding unit tests for filterWithAttrsAndPrefixFunction --- + +// selectorIncludingUID builds a shard selector whose range contains the hash of uid. +func selectorIncludingUID(uid string) sharding.Selector { + hash := "0x" + sharding.HashField(uid) + return sharding.NewSelector(sharding.ShardRangeRequirement{ + Key: "object.metadata.uid", + Start: hash, + End: incrementHex(hash), + }) +} + +// selectorExcludingUID builds a shard selector whose range does NOT contain the hash of uid. +func selectorExcludingUID(uid string) sharding.Selector { + hash := "0x" + sharding.HashField(uid) + return sharding.NewSelector(sharding.ShardRangeRequirement{ + Key: "object.metadata.uid", + Start: "0x0000000000000000", + End: hash, + }) +} + +func incrementHex(hex string) string { + b := []byte(hex) + for i := len(b) - 1; i >= 2; i-- { + if b[i] < '9' { + b[i]++ + return string(b) + } else if b[i] == '9' { + b[i] = 'a' + return string(b) + } else if b[i] < 'f' { + b[i]++ + return string(b) + } + b[i] = '0' + } + return "0x10000000000000000" +} + +func makeExamplePod(name, namespace, uid string, podLabels map[string]string) *example.Pod { + return &example.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + UID: types.UID(uid), + Labels: podLabels, + }, + } +} + +func TestFilterWithAttrsAndPrefixFunction_ShardMatchAndLabels(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + uid := "test-uid-1" + pod := makeExamplePod("pod1", "default", uid, map[string]string{"app": "web"}) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"app": "web"}), + Field: fields.Everything(), + ShardSelector: selectorIncludingUID(uid), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if !filter("/pods/default/pod1", labels.Set{"app": "web"}, fields.Set{"metadata.name": "pod1", "metadata.namespace": "default"}, pod) { + t.Error("expected filter to match: shard matches and labels match") + } +} + +func TestFilterWithAttrsAndPrefixFunction_ShardMatchButLabelMismatch(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + uid := "test-uid-2" + pod := makeExamplePod("pod2", "default", uid, map[string]string{"app": "api"}) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"app": "web"}), + Field: fields.Everything(), + ShardSelector: selectorIncludingUID(uid), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if filter("/pods/default/pod2", labels.Set{"app": "api"}, fields.Set{"metadata.name": "pod2", "metadata.namespace": "default"}, pod) { + t.Error("expected filter to reject: shard matches but labels don't") + } +} + +func TestFilterWithAttrsAndPrefixFunction_ShardMismatch(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + uid := "test-uid-3" + pod := makeExamplePod("pod3", "default", uid, map[string]string{"app": "web"}) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: selectorExcludingUID(uid), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if filter("/pods/default/pod3", labels.Set{"app": "web"}, fields.Set{"metadata.name": "pod3", "metadata.namespace": "default"}, pod) { + t.Error("expected filter to reject: shard does not match") + } +} + +func TestFilterWithAttrsAndPrefixFunction_NilShardSelector(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + pod := makeExamplePod("pod4", "default", "uid-4", map[string]string{"app": "web"}) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.SelectorFromSet(labels.Set{"app": "web"}), + Field: fields.Everything(), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if !filter("/pods/default/pod4", labels.Set{"app": "web"}, fields.Set{"metadata.name": "pod4", "metadata.namespace": "default"}, pod) { + t.Error("expected filter to match: nil ShardSelector should pass everything through") + } +} + +func TestFilterWithAttrsAndPrefixFunction_EverythingSelector(t *testing.T) { + pod := makeExamplePod("pod5", "default", "uid-5", nil) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: sharding.Everything(), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if !filter("/pods/default/pod5", labels.Set{}, fields.Set{"metadata.name": "pod5", "metadata.namespace": "default"}, pod) { + t.Error("expected filter to match: Everything() selector should pass all objects") + } +} + +func TestFilterWithAttrsAndPrefixFunction_WrongPrefix(t *testing.T) { + uid := "test-uid-6" + pod := makeExamplePod("pod6", "default", uid, nil) + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: selectorIncludingUID(uid), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if filter("/configmaps/default/cm1", nil, nil, pod) { + t.Error("expected filter to reject: key prefix doesn't match") + } +} + +func TestFilterWithAttrsAndPrefixFunction_ShardErrorDropsEvent(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + obj := &runtime.Unknown{} + gr := schema.GroupResource{Resource: "pods"} + + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: sharding.NewSelector(sharding.ShardRangeRequirement{ + Key: "object.metadata.uid", + Start: "0x0000000000000000", + End: "0x10000000000000000", + }), + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if filter("/pods/default/pod-err", nil, nil, obj) { + t.Error("expected filter to reject: shard matching should fail on unstructured object") + } +} + +func TestFilterWithAttrsAndPrefixFunction_NamespaceSharding(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + ns := "my-namespace" + pod := makeExamplePod("pod-ns", ns, "some-uid", nil) + + nsHash := "0x" + sharding.HashField(ns) + sel := sharding.NewSelector(sharding.ShardRangeRequirement{ + Key: "object.metadata.namespace", + Start: nsHash, + End: incrementHex(nsHash), + }) + + gr := schema.GroupResource{Resource: "pods"} + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: sel, + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if !filter("/pods/my-namespace/pod-ns", labels.Set{}, fields.Set{"metadata.name": "pod-ns", "metadata.namespace": ns}, pod) { + t.Error("expected filter to match: namespace-based shard selector should match") + } +} + +func TestFilterWithAttrsAndPrefixFunction_NamespaceShardingMismatch(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ShardedListAndWatch, true) + ns := "other-namespace" + pod := makeExamplePod("pod-ns2", ns, "some-uid-2", nil) + + targetHash := "0x" + sharding.HashField("my-namespace") + sel := sharding.NewSelector(sharding.ShardRangeRequirement{ + Key: "object.metadata.namespace", + Start: targetHash, + End: incrementHex(targetHash), + }) + + gr := schema.GroupResource{Resource: "pods"} + pred := storage.SelectionPredicate{ + Label: labels.Everything(), + Field: fields.Everything(), + ShardSelector: sel, + GetAttrs: storage.DefaultNamespaceScopedAttr, + } + filter := filterWithAttrsAndPrefixFunction("/pods/", pred, gr) + + if filter("/pods/other-namespace/pod-ns2", labels.Set{}, fields.Set{"metadata.name": "pod-ns2", "metadata.namespace": ns}, pod) { + t.Error("expected filter to reject: namespace hash doesn't fall in shard range") + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/caching_object.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/caching_object.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/caching_object.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/caching_object.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/caching_object_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/caching_object_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/caching_object_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/caching_object_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/compactor.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/compactor.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/compactor.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/compactor.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator.go index 68d269ca3..4e1fabfab 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator.go @@ -39,6 +39,7 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/cacher/delegator" "k8s.io/apiserver/pkg/storage/cacher/metrics" + "k8s.io/apiserver/pkg/storage/cacher/store" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/component-base/tracing" "k8s.io/klog/v2" @@ -99,8 +100,8 @@ func (c *CacheDelegator) GetCurrentResourceVersion(ctx context.Context) (uint64, return c.storage.GetCurrentResourceVersion(ctx) } -func (c *CacheDelegator) SetKeysFunc(keys storage.KeysFunc) { - c.storage.SetKeysFunc(keys) +func (c *CacheDelegator) EnableResourceSizeEstimation(keys storage.KeysFunc) error { + return c.storage.EnableResourceSizeEstimation(keys) } func (c *CacheDelegator) Delete(ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object, opts storage.DeleteOptions) error { @@ -111,7 +112,7 @@ func (c *CacheDelegator) Delete(ctx context.Context, key string, out runtime.Obj } else if exists { // DeepCopy the object since we modify resource version when serializing the // current object. - currObj := elem.(*storeElement).Object.DeepCopyObject() + currObj := elem.(*store.Element).Object.DeepCopyObject() return c.storage.Delete(ctx, key, out, preconditions, validateDeletion, currObj, opts) } // If we couldn't get the object, fallback to no-suggestion. @@ -129,10 +130,6 @@ func (c *CacheDelegator) Watch(ctx context.Context, key string, opts storage.Lis if !utilfeature.DefaultFeatureGate.Enabled(features.WatchList) && opts.SendInitialEvents != nil { opts.SendInitialEvents = nil } - // TODO: we should eventually get rid of this legacy case - if utilfeature.DefaultFeatureGate.Enabled(features.WatchFromStorageWithoutResourceVersion) && opts.SendInitialEvents == nil && opts.ResourceVersion == "" { - return c.storage.Watch(ctx, key, opts) - } return c.cacher.Watch(ctx, key, opts) } @@ -254,7 +251,7 @@ func (c *CacheDelegator) GuaranteedUpdate(ctx context.Context, key string, desti } else if exists { // DeepCopy the object since we modify resource version when serializing the // current object. - currObj := elem.(*storeElement).Object.DeepCopyObject() + currObj := elem.(*store.Element).Object.DeepCopyObject() return c.storage.GuaranteedUpdate(ctx, key, destination, ignoreNotFound, preconditions, tryUpdate, currObj) } // If we couldn't get the object, fallback to no-suggestion. @@ -344,10 +341,10 @@ func (c *consistencyChecker) check(ctx context.Context) { c.cacher.MarkConsistent(true) return } - klog.ErrorS(nil, "Cache consistency check failed", "group", c.groupResource.Group, "resource", c.groupResource.Resource, "resourceVersion", digests.ResourceVersion, "etcdDigest", digests.EtcdDigest, "cacheDigest", digests.CacheDigest) + klog.ErrorS(nil, "Cache consistency check failed", "group", c.groupResource.Group, "resource", c.groupResource.Resource, "resourceVersion", digests.ResourceVersion, "etcdDigest", digests.EtcdDigest, "cacheDigest", digests.CacheDigest, "diffDetail", digests.DiffDetail) metrics.StorageConsistencyCheckTotal.WithLabelValues(c.groupResource.Group, c.groupResource.Resource, "failure").Inc() if panicOnCacheInconsistency { - panic(fmt.Sprintf("Cache consistency check failed, group: %q, resource: %q, resourceVersion: %q, etcdDigest: %q, cacheDigest: %q", c.groupResource.Group, c.groupResource.Resource, digests.ResourceVersion, digests.EtcdDigest, digests.CacheDigest)) + panic(fmt.Sprintf("Cache consistency check failed, group: %q, resource: %q, resourceVersion: %q, etcdDigest: %q, cacheDigest: %q, diffDetail: %v", c.groupResource.Group, c.groupResource.Resource, digests.ResourceVersion, digests.EtcdDigest, digests.CacheDigest, digests.DiffDetail)) } c.cacher.MarkConsistent(false) } @@ -356,14 +353,43 @@ func (c *consistencyChecker) calculateDigests(ctx context.Context) (*storageDige if !c.cacher.Ready() { return nil, fmt.Errorf("cache is not ready") } - cacheDigest, cacheResourceVersion, err := c.calculateStoreDigest(ctx, c.cacher, "0", 0) + // variables for tracking the diff in consistency checker + cacheItems := []namespaceNameRV{} + var foundDiff *diffDetail + var etcdIndex int + + cacheDigest, cacheResourceVersion, err := c.calculateStoreDigest(ctx, c.cacher, "0", 0, func(obj metav1.Object) { + // collect items from cacher's list + cacheItems = append(cacheItems, namespaceNameRV{Namespace: obj.GetNamespace(), Name: obj.GetName(), RV: obj.GetResourceVersion()}) + }) if err != nil { return nil, fmt.Errorf("failed calculating cache digest: %w", err) } - etcdDigest, etcdResourceVersion, err := c.calculateStoreDigest(ctx, c.etcd, cacheResourceVersion, storageWatchListPageSize) + etcdDigest, etcdResourceVersion, err := c.calculateStoreDigest(ctx, c.etcd, cacheResourceVersion, storageWatchListPageSize, func(obj metav1.Object) { + // compare the item in etcd's list against cacheItems which is cacher's list + if foundDiff != nil { + return + } + etcdItem := namespaceNameRV{Namespace: obj.GetNamespace(), Name: obj.GetName(), RV: obj.GetResourceVersion()} + if len(cacheItems) <= etcdIndex { + foundDiff = &diffDetail{Index: etcdIndex, EtcdItem: &etcdItem} + cacheItems = nil // don't need it any more and nil it to allow GC to collect it + return + } + if cacheItem := cacheItems[etcdIndex]; cacheItem != etcdItem { + foundDiff = &diffDetail{Index: etcdIndex, EtcdItem: &etcdItem, CacheItem: &cacheItem} + cacheItems = nil // don't need it any more and nil it to allow GC to collect it + return + } + etcdIndex += 1 + }) if err != nil { return nil, fmt.Errorf("failed calculating etcd digest: %w", err) } + if len(cacheItems) > etcdIndex { + cacheItem := cacheItems[etcdIndex] + foundDiff = &diffDetail{Index: etcdIndex, CacheItem: &cacheItem} + } if cacheResourceVersion != etcdResourceVersion { return nil, fmt.Errorf("etcd returned different resource version then expected, cache: %q, etcd: %q", cacheResourceVersion, etcdResourceVersion) } @@ -371,16 +397,30 @@ func (c *consistencyChecker) calculateDigests(ctx context.Context) (*storageDige ResourceVersion: cacheResourceVersion, CacheDigest: cacheDigest, EtcdDigest: etcdDigest, + DiffDetail: foundDiff, }, nil } +type namespaceNameRV struct { + Namespace string + Name string + RV string +} + +type diffDetail struct { + Index int + CacheItem *namespaceNameRV + EtcdItem *namespaceNameRV +} + type storageDigest struct { ResourceVersion string CacheDigest string EtcdDigest string + DiffDetail *diffDetail } -func (c *consistencyChecker) calculateStoreDigest(ctx context.Context, store getLister, resourceVersion string, limit int64) (digest, rv string, err error) { +func (c *consistencyChecker) calculateStoreDigest(ctx context.Context, store getLister, resourceVersion string, limit int64, metaVisitor func(objectMeta metav1.Object)) (digest, rv string, err error) { opts := storage.ListOptions{ Recursive: true, Predicate: storage.Everything, @@ -399,7 +439,7 @@ func (c *consistencyChecker) calculateStoreDigest(ctx context.Context, store get if err != nil { return "", "", err } - err = addListToDigest(h, resp) + err = addListToDigest(h, resp, metaVisitor) if err != nil { return "", "", err } @@ -420,7 +460,7 @@ func (c *consistencyChecker) calculateStoreDigest(ctx context.Context, store get } } -func addListToDigest(h hash.Hash64, list runtime.Object) error { +func addListToDigest(h hash.Hash64, list runtime.Object, metaVisitor func(objectMeta metav1.Object)) error { return meta.EachListItem(list, func(obj runtime.Object) error { objectMeta, err := meta.Accessor(obj) if err != nil { @@ -430,6 +470,7 @@ func addListToDigest(h hash.Hash64, list runtime.Object) error { if err != nil { return err } + metaVisitor(objectMeta) return nil }) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator_test.go similarity index 77% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator_test.go index 8fd01d6f1..a3fc672a3 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/delegator_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/delegator_test.go @@ -19,6 +19,7 @@ package cacher import ( "context" "fmt" + "reflect" "testing" "github.com/google/go-cmp/cmp" @@ -29,6 +30,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/apis/example" "k8s.io/apiserver/pkg/storage" + + cachertesting "k8s.io/apiserver/pkg/storage/cacher/testing" ) func TestConsistencyCheckerDigest(t *testing.T) { @@ -94,6 +97,11 @@ func TestConsistencyCheckerDigest(t *testing.T) { ResourceVersion: "2", CacheDigest: "4ae4e750bd825b17", EtcdDigest: "f940a60af965b03", + DiffDetail: &diffDetail{ + Index: 0, + CacheItem: &namespaceNameRV{Namespace: "kube-system", Name: "pod", RV: "2"}, + EtcdItem: &namespaceNameRV{Namespace: "kube-public", Name: "pod", RV: "2"}, + }, }, expectConsistent: false, }, @@ -111,6 +119,11 @@ func TestConsistencyCheckerDigest(t *testing.T) { ResourceVersion: "2", CacheDigest: "c9120494e4c1897d", EtcdDigest: "c9156494e4c46274", + DiffDetail: &diffDetail{ + Index: 0, + CacheItem: &namespaceNameRV{Namespace: "default", Name: "pod2", RV: "2"}, + EtcdItem: &namespaceNameRV{Namespace: "default", Name: "pod3", RV: "2"}, + }, }, expectConsistent: false, }, @@ -128,6 +141,11 @@ func TestConsistencyCheckerDigest(t *testing.T) { ResourceVersion: "4", CacheDigest: "86bf3a5e80d1c5ca", EtcdDigest: "86bf3a5e80d1c5cd", + DiffDetail: &diffDetail{ + Index: 0, + CacheItem: &namespaceNameRV{Namespace: "default", Name: "pod", RV: "3"}, + EtcdItem: &namespaceNameRV{Namespace: "default", Name: "pod", RV: "4"}, + }, }, expectConsistent: false, }, @@ -146,6 +164,34 @@ func TestConsistencyCheckerDigest(t *testing.T) { ResourceVersion: "3", CacheDigest: "1859bac707c2cb2b", EtcdDigest: "11d147fc800df0e0", + DiffDetail: &diffDetail{ + Index: 1, + CacheItem: nil, + EtcdItem: &namespaceNameRV{Namespace: "Default", Name: "pod", RV: "3"}, + }, + }, + expectConsistent: false, + }, + { + desc: "watch missed delete event", + resourceVersion: "3", + cacherReady: true, + cacherItems: []example.Pod{ + {ObjectMeta: metav1.ObjectMeta{Namespace: "Default", Name: "pod", ResourceVersion: "2"}}, + {ObjectMeta: metav1.ObjectMeta{Namespace: "Default", Name: "pod", ResourceVersion: "3"}}, + }, + etcdItems: []example.Pod{ + {ObjectMeta: metav1.ObjectMeta{Namespace: "Default", Name: "pod", ResourceVersion: "2"}}, + }, + expectDigest: storageDigest{ + ResourceVersion: "3", + CacheDigest: "11d147fc800df0e0", + EtcdDigest: "1859bac707c2cb2b", + DiffDetail: &diffDetail{ + Index: 1, + CacheItem: &namespaceNameRV{Namespace: "Default", Name: "pod", RV: "3"}, + EtcdItem: nil, + }, }, expectConsistent: false, }, @@ -153,8 +199,8 @@ func TestConsistencyCheckerDigest(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - etcd := &dummyStorage{ - getListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + etcd := &cachertesting.MockStorage{ + GetListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { if key != tc.expectListKey { t.Fatalf("Expect GetList key %q, got %q", tc.expectListKey, key) } @@ -170,11 +216,11 @@ func TestConsistencyCheckerDigest(t *testing.T) { return nil }, } - cacher := &dummyCacher{ - ready: tc.cacherReady, - consistent: true, - dummyStorage: dummyStorage{ - getListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + cacher := &cachertesting.MockCacher{ + IsReady: tc.cacherReady, + Consistent: true, + MockStorage: cachertesting.MockStorage{ + GetListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { if key != tc.expectListKey { t.Fatalf("Expect GetList key %q, got %q", tc.expectListKey, key) } @@ -199,13 +245,13 @@ func TestConsistencyCheckerDigest(t *testing.T) { if err != nil { return } - if *digest != tc.expectDigest { + if !reflect.DeepEqual(*digest, tc.expectDigest) { t.Errorf("Expect: %+v Got: %+v", &tc.expectDigest, *digest) } checker.check(context.Background()) - if cacher.consistent != tc.expectConsistent { - t.Errorf("Expect: %+v Got: %+v", tc.expectConsistent, cacher.consistent) + if cacher.Consistent != tc.expectConsistent { + t.Errorf("Expect: %+v Got: %+v", tc.expectConsistent, cacher.Consistent) } }) } @@ -216,8 +262,8 @@ func TestConsistencyCheckerListOpts(t *testing.T) { resourceVersion := "50" etcdOpts := []storage.ListOptions{} - etcd := &dummyStorage{ - getListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + etcd := &cachertesting.MockStorage{ + GetListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { etcdOpts = append(etcdOpts, opts) podList := listObj.(*example.PodList) podList.ResourceVersion = resourceVersion @@ -225,10 +271,10 @@ func TestConsistencyCheckerListOpts(t *testing.T) { }, } cacherOpts := []storage.ListOptions{} - cacher := &dummyCacher{ - ready: true, - dummyStorage: dummyStorage{ - getListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { + cacher := &cachertesting.MockCacher{ + IsReady: true, + MockStorage: cachertesting.MockStorage{ + GetListFn: func(_ context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { cacherOpts = append(cacherOpts, opts) podList := listObj.(*example.PodList) podList.ResourceVersion = resourceVersion @@ -287,7 +333,7 @@ func TestConsistencyCheckerDigestMatches(t *testing.T) { t.Log("Execute list to ensure cache is up to date") outList := &example.PodList{} - err := store.cacher.GetList(ctx, "/pods", storage.ListOptions{ResourceVersion: resourceVersion, Recursive: true, Predicate: storage.Everything, ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan}, outList) + err := store.cacher.GetList(ctx, "/pods/", storage.ListOptions{ResourceVersion: resourceVersion, Recursive: true, Predicate: storage.Everything, ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan}, outList) if err != nil { t.Fatal(err) } @@ -295,7 +341,7 @@ func TestConsistencyCheckerDigestMatches(t *testing.T) { t.Errorf("Expect to get %d pods, got %d", storageWatchListPageSize+1, len(outList.Items)) } - checker := newConsistencyChecker("/pods", schema.GroupResource{}, store.cacher.newListFunc, store.cacher, store.storage) + checker := newConsistencyChecker("/pods/", schema.GroupResource{}, store.cacher.newListFunc, store.cacher, store.storage) digest, err := checker.calculateDigests(ctx) if err != nil { t.Fatal(err) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher.go similarity index 64% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher.go index d5ac30152..e037ed4ab 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher.go @@ -31,6 +31,8 @@ import ( "k8s.io/apiserver/pkg/storage" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/consistencydetector" + "k8s.io/client-go/util/watchlist" ) // listerWatcher opaques storage.Interface to expose cache.ListerWatcher. @@ -39,15 +41,20 @@ type listerWatcher struct { resourcePrefix string newListFunc func() runtime.Object contextMetadata metadata.MD + + unsupportedWatchListSemantics bool + watchListConsistencyCheckEnabled bool } // NewListerWatcher returns a storage.Interface backed ListerWatcher. func NewListerWatcher(storage storage.Interface, resourcePrefix string, newListFunc func() runtime.Object, contextMetadata metadata.MD) cache.ListerWatcher { return &listerWatcher{ - storage: storage, - resourcePrefix: resourcePrefix, - newListFunc: newListFunc, - contextMetadata: contextMetadata, + storage: storage, + resourcePrefix: resourcePrefix, + newListFunc: newListFunc, + contextMetadata: contextMetadata, + unsupportedWatchListSemantics: watchlist.DoesClientNotSupportWatchListSemantics(storage), + watchListConsistencyCheckEnabled: consistencydetector.IsDataConsistencyDetectionForWatchListEnabled(), } } @@ -66,6 +73,27 @@ func (lw *listerWatcher) List(options metav1.ListOptions) (runtime.Object, error Predicate: pred, Recursive: true, } + + // The ConsistencyChecker built into reflectors for the WatchList feature is responsible + // for verifying that the data received from the server (potentially from the watch cache) + // is consistent with the data stored in etcd. + // + // To perform this verification, the checker uses the ResourceVersion obtained from the initial request + // and sets the ResourceVersionMatch so that it retrieves exactly the same data directly from etcd. + // This allows comparing both data sources and confirming their consistency. + // + // The code below checks whether the incoming request originates from the ConsistencyChecker. + // If so, it allows explicitly setting the ResourceVersion. + // + // As of Oct 2025, reflector on its own is not setting RVM=Exact. + // However, even if that changes in the meantime, we would have to propagate that + // down to storage to ensure the correct semantics of the request. + watchListEnabled := utilfeature.DefaultFeatureGate.Enabled(features.WatchList) + supportedRVM := options.ResourceVersionMatch == metav1.ResourceVersionMatchExact + if watchListEnabled && lw.watchListConsistencyCheckEnabled && supportedRVM { + storageOpts.ResourceVersion = options.ResourceVersion + } + ctx := context.Background() if lw.contextMetadata != nil { ctx = metadata.NewOutgoingContext(ctx, lw.contextMetadata) @@ -105,3 +133,7 @@ func (lw *listerWatcher) Watch(options metav1.ListOptions) (watch.Interface, err return lw.storage.Watch(ctx, lw.resourcePrefix, opts) } + +func (lw *listerWatcher) IsWatchListSemanticsUnSupported() bool { + return lw.unsupportedWatchListSemantics +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher_test.go similarity index 65% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher_test.go index 247fd85cc..1ec786e7c 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/lister_watcher_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/lister_watcher_test.go @@ -25,15 +25,33 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/apiserver/pkg/apis/example" "k8s.io/apiserver/pkg/features" + "k8s.io/apiserver/pkg/storage" storagetesting "k8s.io/apiserver/pkg/storage/testing" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/watchlist" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/utils/ptr" + + cachertesting "k8s.io/apiserver/pkg/storage/cacher/testing" ) +func TestDoesClientSupportWatchListSemanticsForKubeClient(t *testing.T) { + target1 := &cachertesting.MockStorage{} + if !watchlist.DoesClientNotSupportWatchListSemantics(target1) { + t.Fatalf("cachertesting.MockStorage should NOT support WatchList semantics") + } + + server, target2 := newEtcdTestStorage(t, "/pods/") + defer server.Terminate(t) + + if watchlist.DoesClientNotSupportWatchListSemantics(target2) { + t.Fatalf("etcd should support WatchList semantics") + } +} + func TestCacherListerWatcher(t *testing.T) { - prefix := "pods" + prefix := "/pods/" fn := func() runtime.Object { return &example.PodList{} } server, store := newEtcdTestStorage(t, prefix) defer server.Terminate(t) @@ -67,7 +85,7 @@ func TestCacherListerWatcher(t *testing.T) { } func TestCacherListerWatcherPagination(t *testing.T) { - prefix := "pods" + prefix := "/pods/" fn := func() runtime.Object { return &example.PodList{} } server, store := newEtcdTestStorage(t, prefix) defer server.Terminate(t) @@ -130,7 +148,7 @@ func TestCacherListerWatcherPagination(t *testing.T) { func TestCacherListerWatcherListWatch(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, true) - prefix := "pods" + prefix := "/pods/" fn := func() runtime.Object { return &example.PodList{} } server, store := newEtcdTestStorage(t, prefix) defer server.Terminate(t) @@ -181,7 +199,7 @@ func TestCacherListerWatcherListWatch(t *testing.T) { func TestCacherListerWatcherWhenListWatchDisabled(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, false) - prefix := "pods" + prefix := "/pods/" fn := func() runtime.Object { return &example.PodList{} } server, store := newEtcdTestStorage(t, prefix) defer server.Terminate(t) @@ -203,3 +221,79 @@ func TestCacherListerWatcherWhenListWatchDisabled(t *testing.T) { t.Fatalf("Expected error %q, but got %q", expectedErrMsg, err.Error()) } } + +func TestListerWatcherListResourceVersionPropagation(t *testing.T) { + scenarios := []struct { + name string + options metav1.ListOptions + watchListEnabled bool + watchListConsistencyCheckEnabled bool + expectedStorageResourceVer string + }{ + { + name: "WatchList FG disabled - RV not propagated", + options: metav1.ListOptions{ + ResourceVersion: "123", + ResourceVersionMatch: metav1.ResourceVersionMatchExact, + }, + watchListEnabled: false, + watchListConsistencyCheckEnabled: true, + expectedStorageResourceVer: "", + }, + { + name: "WatchList consistency check disabled - RV not propagated", + options: metav1.ListOptions{ + ResourceVersion: "123", + ResourceVersionMatch: metav1.ResourceVersionMatchExact, + }, + watchListEnabled: true, + watchListConsistencyCheckEnabled: false, + expectedStorageResourceVer: "", + }, + { + name: "Unsupported RVM - RV not propagated", + options: metav1.ListOptions{ + ResourceVersion: "123", + ResourceVersionMatch: metav1.ResourceVersionMatchNotOlderThan, + }, + watchListEnabled: true, + watchListConsistencyCheckEnabled: true, + expectedStorageResourceVer: "", + }, + { + name: "all conditions satisfied - RV propagated", + options: metav1.ListOptions{ + ResourceVersion: "123", + ResourceVersionMatch: metav1.ResourceVersionMatchExact, + }, + watchListEnabled: true, + watchListConsistencyCheckEnabled: true, + expectedStorageResourceVer: "123", + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.WatchList, scenario.watchListEnabled) + + backingStorage := &cachertesting.MockStorage{} + var capturedOpts storage.ListOptions + backingStorage.GetListFn = func(ctx context.Context, resPrefix string, opts storage.ListOptions, listObj runtime.Object) error { + capturedOpts = opts + return nil + } + + targetInterface := NewListerWatcher(backingStorage, "/pods/", func() runtime.Object { return &example.PodList{} }, nil) + target := targetInterface.(*listerWatcher) + target.watchListConsistencyCheckEnabled = scenario.watchListConsistencyCheckEnabled + + if _, err := target.List(scenario.options); err != nil { + t.Fatalf("List returned error: %v", err) + } + + if capturedOpts.ResourceVersion != scenario.expectedStorageResourceVer { + t.Fatalf("expected storage ResourceVersion %q, got %q", scenario.expectedStorageResourceVer, capturedOpts.ResourceVersion) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/metrics/metrics.go similarity index 80% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/metrics/metrics.go index 0f507cf12..0981d4573 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/metrics/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/metrics/metrics.go @@ -20,6 +20,8 @@ import ( "sync" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" compbasemetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) @@ -112,7 +114,7 @@ var ( Namespace: namespace, Subsystem: subsystem, Name: "resource_version", - Help: "Current resource version of watch cache broken by resource type.", + Help: "Current resource version of watch cache broken by resource type. This is truncated to the 15 least significant digits.", StabilityLevel: compbasemetrics.ALPHA, }, []string{"group", "resource"}, @@ -185,6 +187,26 @@ var ( Help: "Counter for status of consistency checks between etcd and watch cache", StabilityLevel: compbasemetrics.ALPHA, }, []string{"group", "resource", "status"}) + + WatchShardsTotal = compbasemetrics.NewGaugeVec( + &compbasemetrics.GaugeOpts{ + Namespace: namespace, + Name: "watch_shards_total", + Help: "Number of active sharded watch connections broken by resource type.", + StabilityLevel: compbasemetrics.ALPHA, + }, + []string{"group", "resource"}, + ) + + WatchFilteredEventsTotal = compbasemetrics.NewCounterVec( + &compbasemetrics.CounterOpts{ + Namespace: namespace, + Name: "watch_filtered_events_total", + Help: "Counter of events filtered out by shard selector during watch dispatch, broken by resource type.", + StabilityLevel: compbasemetrics.ALPHA, + }, + []string{"group", "resource"}, + ) ) var registerMetrics sync.Once @@ -208,6 +230,10 @@ func Register() { legacyregistry.MustRegister(WatchCacheReadWait) legacyregistry.MustRegister(ConsistentReadTotal) legacyregistry.MustRegister(StorageConsistencyCheckTotal) + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + legacyregistry.MustRegister(WatchShardsTotal) + legacyregistry.MustRegister(WatchFilteredEventsTotal) + } }) } @@ -219,11 +245,28 @@ func RecordListCacheMetrics(groupResource schema.GroupResource, indexName string } // RecordResourceVersion sets the current resource version for a given resource type. +// The resource version is truncated to the 15 least significant digits to prevent +// the metric from growing indefinitely and losing precision when it exceeds 2^53-1. func RecordResourceVersion(groupResource schema.GroupResource, resourceVersion uint64) { - watchCacheResourceVersion.WithLabelValues(groupResource.Group, groupResource.Resource).Set(float64(resourceVersion)) + watchCacheResourceVersion.WithLabelValues(groupResource.Group, groupResource.Resource).Set(float64(resourceVersion % 1000000000000000)) +} + +// RecordShardedWatchStarted increments the active sharded watch gauge for the given resource. +func RecordShardedWatchStarted(groupResource schema.GroupResource) { + WatchShardsTotal.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() +} + +// RecordShardedWatchStopped decrements the active sharded watch gauge for the given resource. +func RecordShardedWatchStopped(groupResource schema.GroupResource) { + WatchShardsTotal.WithLabelValues(groupResource.Group, groupResource.Resource).Dec() +} + +// RecordWatchFilteredEvent increments the counter for events filtered by shard selector. +func RecordWatchFilteredEvent(groupResource schema.GroupResource) { + WatchFilteredEventsTotal.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() } -// RecordsWatchCacheCapacityChange record watchCache capacity resize(increase or decrease) operations. +// RecordsWatchCacheCapacityChange records watchCache capacity resize(increase or decrease) operations. func RecordsWatchCacheCapacityChange(groupResource schema.GroupResource, old, new int) { WatchCacheCapacity.WithLabelValues(groupResource.Group, groupResource.Resource).Set(float64(new)) if old < new { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/progress/watch_progress.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/progress/watch_progress.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/progress/watch_progress.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/progress/watch_progress.go index 76fe73f13..9880fc6c3 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/progress/watch_progress.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/progress/watch_progress.go @@ -71,7 +71,7 @@ func (pr *ConditionalProgressRequester) Run(stopCh <-chan struct{}) { ctx = metadata.NewOutgoingContext(ctx, pr.contextMetadata) } go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) <-stopCh pr.mux.Lock() defer pr.mux.Unlock() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/progress/watch_progress_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/progress/watch_progress_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/progress/watch_progress_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/progress/watch_progress_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/ready.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/ready.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/ready.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/ready.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/ready_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/ready_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/ready_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/ready_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store.go index 9ec654e7d..00dbc2129 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cacher +package store import ( "fmt" @@ -60,7 +60,7 @@ const ( btreeDegree = 16 ) -type storeIndexer interface { +type Indexer interface { Add(obj interface{}) error Update(obj interface{}) error Delete(obj interface{}) error @@ -72,17 +72,17 @@ type storeIndexer interface { ByIndex(indexName, indexedValue string) ([]interface{}, error) } -type orderedLister interface { +type OrderedLister interface { ListPrefix(prefix, continueKey string) []interface{} Count(prefix, continueKey string) (count int) - Clone() orderedLister + Clone() OrderedLister } -func newStoreIndexer(indexers *cache.Indexers) storeIndexer { +func NewIndexer(indexers *cache.Indexers) Indexer { if utilfeature.DefaultFeatureGate.Enabled(features.BtreeWatchCache) { - return newThreadedBtreeStoreIndexer(storeElementIndexers(indexers), btreeDegree) + return newThreadedBtreeStoreIndexer(ElementIndexers(indexers), btreeDegree) } - return cache.NewIndexer(storeElementKey, storeElementIndexers(indexers)) + return cache.NewIndexer(ElementKey, ElementIndexers(indexers)) } // Computing a key of an object is generally non-trivial (it performs @@ -90,32 +90,32 @@ func newStoreIndexer(indexers *cache.Indexers) storeIndexer { // labels. To avoid computing them multiple times (to serve the event // in different List/Watch requests), in the underlying store we are // keeping structs (key, object, labels, fields). -type storeElement struct { +type Element struct { Key string Object runtime.Object Labels labels.Set Fields fields.Set } -func storeElementKey(obj interface{}) (string, error) { - elem, ok := obj.(*storeElement) +func ElementKey(obj interface{}) (string, error) { + elem, ok := obj.(*Element) if !ok { return "", fmt.Errorf("not a storeElement: %v", obj) } return elem.Key, nil } -func storeElementObject(obj interface{}) (runtime.Object, error) { - elem, ok := obj.(*storeElement) +func ElementObject(obj interface{}) (runtime.Object, error) { + elem, ok := obj.(*Element) if !ok { return nil, fmt.Errorf("not a storeElement: %v", obj) } return elem.Object, nil } -func storeElementIndexFunc(objIndexFunc cache.IndexFunc) cache.IndexFunc { +func ElementIndexFunc(objIndexFunc cache.IndexFunc) cache.IndexFunc { return func(obj interface{}) (strings []string, e error) { - seo, err := storeElementObject(obj) + seo, err := ElementObject(obj) if err != nil { return nil, err } @@ -123,13 +123,13 @@ func storeElementIndexFunc(objIndexFunc cache.IndexFunc) cache.IndexFunc { } } -func storeElementIndexers(indexers *cache.Indexers) cache.Indexers { +func ElementIndexers(indexers *cache.Indexers) cache.Indexers { if indexers == nil { return cache.Indexers{} } ret := cache.Indexers{} for indexName, indexFunc := range *indexers { - ret[indexName] = storeElementIndexFunc(indexFunc) + ret[indexName] = ElementIndexFunc(indexFunc) } return ret } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree.go index 9908e2f64..33e8e4f10 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cacher +package store import ( "fmt" "strings" "sync" - "github.com/google/btree" "k8s.io/client-go/tools/cache" + "k8s.io/utils/third_party/forked/golang/btree" ) // newThreadedBtreeStoreIndexer returns a storage for cacher by adding locking over the two 2 data structures: @@ -43,7 +43,7 @@ type threadedStoreIndexer struct { indexer indexer } -var _ orderedLister = (*threadedStoreIndexer)(nil) +var _ OrderedLister = (*threadedStoreIndexer)(nil) func (si *threadedStoreIndexer) Count(prefix, continueKey string) (count int) { si.lock.RLock() @@ -51,7 +51,7 @@ func (si *threadedStoreIndexer) Count(prefix, continueKey string) (count int) { return si.store.Count(prefix, continueKey) } -func (si *threadedStoreIndexer) Clone() orderedLister { +func (si *threadedStoreIndexer) Clone() OrderedLister { si.lock.RLock() defer si.lock.RUnlock() return si.store.Clone() @@ -69,7 +69,7 @@ func (si *threadedStoreIndexer) addOrUpdate(obj interface{}) error { if obj == nil { return fmt.Errorf("obj cannot be nil") } - newElem, ok := obj.(*storeElement) + newElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -80,7 +80,7 @@ func (si *threadedStoreIndexer) addOrUpdate(obj interface{}) error { } func (si *threadedStoreIndexer) Delete(obj interface{}) error { - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -141,17 +141,17 @@ func (si *threadedStoreIndexer) ByIndex(indexName, indexValue string) ([]interfa func newBtreeStore(degree int) btreeStore { return btreeStore{ - tree: btree.NewG(degree, func(a, b *storeElement) bool { + tree: btree.New(degree, func(a, b *Element) bool { return a.Key < b.Key }), } } type btreeStore struct { - tree *btree.BTreeG[*storeElement] + tree *btree.BTree[*Element] } -func (s *btreeStore) Clone() orderedLister { +func (s *btreeStore) Clone() OrderedLister { return &btreeStore{ tree: s.tree.Clone(), } @@ -161,7 +161,7 @@ func (s *btreeStore) Add(obj interface{}) error { if obj == nil { return fmt.Errorf("obj cannot be nil") } - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -173,7 +173,7 @@ func (s *btreeStore) Update(obj interface{}) error { if obj == nil { return fmt.Errorf("obj cannot be nil") } - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -185,7 +185,7 @@ func (s *btreeStore) Delete(obj interface{}) error { if obj == nil { return fmt.Errorf("obj cannot be nil") } - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -193,13 +193,13 @@ func (s *btreeStore) Delete(obj interface{}) error { return nil } -func (s *btreeStore) deleteElem(storeElem *storeElement) (*storeElement, bool) { +func (s *btreeStore) deleteElem(storeElem *Element) (*Element, bool) { return s.tree.Delete(storeElem) } func (s *btreeStore) List() []interface{} { items := make([]interface{}, 0, s.tree.Len()) - s.tree.Ascend(func(item *storeElement) bool { + s.tree.Ascend(func(item *Element) bool { items = append(items, item) return true }) @@ -208,7 +208,7 @@ func (s *btreeStore) List() []interface{} { func (s *btreeStore) ListKeys() []string { items := make([]string, 0, s.tree.Len()) - s.tree.Ascend(func(item *storeElement) bool { + s.tree.Ascend(func(item *Element) bool { items = append(items, item.Key) return true }) @@ -216,7 +216,7 @@ func (s *btreeStore) ListKeys() []string { } func (s *btreeStore) Get(obj interface{}) (item interface{}, exists bool, err error) { - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return nil, false, fmt.Errorf("obj is not a storeElement") } @@ -231,7 +231,7 @@ func (s *btreeStore) GetByKey(key string) (item interface{}, exists bool, err er func (s *btreeStore) Replace(objs []interface{}, _ string) error { s.tree.Clear(false) for _, obj := range objs { - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -242,13 +242,13 @@ func (s *btreeStore) Replace(objs []interface{}, _ string) error { // addOrUpdateLocked assumes a lock is held and is used for Add // and Update operations. -func (s *btreeStore) addOrUpdateElem(storeElem *storeElement) *storeElement { +func (s *btreeStore) addOrUpdateElem(storeElem *Element) *Element { oldObj, _ := s.tree.ReplaceOrInsert(storeElem) return oldObj } func (s *btreeStore) getByKey(key string) (item interface{}, exists bool, err error) { - keyElement := &storeElement{Key: key} + keyElement := &Element{Key: key} item, exists = s.tree.Get(keyElement) return item, exists, nil } @@ -258,7 +258,7 @@ func (s *btreeStore) ListPrefix(prefix, continueKey string) []interface{} { continueKey = prefix } var result []interface{} - s.tree.AscendGreaterOrEqual(&storeElement{Key: continueKey}, func(item *storeElement) bool { + s.tree.AscendGreaterOrEqual(&Element{Key: continueKey}, func(item *Element) bool { if !strings.HasPrefix(item.Key, prefix) { return false } @@ -272,7 +272,7 @@ func (s *btreeStore) Count(prefix, continueKey string) (count int) { if continueKey == "" { continueKey = prefix } - s.tree.AscendGreaterOrEqual(&storeElement{Key: continueKey}, func(item *storeElement) bool { + s.tree.AscendGreaterOrEqual(&Element{Key: continueKey}, func(item *Element) bool { if !strings.HasPrefix(item.Key, prefix) { return false } @@ -292,13 +292,13 @@ func (s *btreeStore) Count(prefix, continueKey string) (count int) { // Difference in mutability of stored values is used for optimizing some operations in client-go Indexer. func newIndexer(indexers cache.Indexers) indexer { return indexer{ - indices: map[string]map[string]map[string]*storeElement{}, + indices: map[string]map[string]map[string]*Element{}, indexers: indexers, } } type indexer struct { - indices map[string]map[string]map[string]*storeElement + indices map[string]map[string]map[string]*Element indexers cache.Indexers } @@ -317,9 +317,9 @@ func (i *indexer) ByIndex(indexName, indexValue string) ([]interface{}, error) { } func (i *indexer) Replace(objs []interface{}, resourceVersion string) error { - i.indices = map[string]map[string]map[string]*storeElement{} + i.indices = map[string]map[string]map[string]*Element{} for _, obj := range objs { - storeElem, ok := obj.(*storeElement) + storeElem, ok := obj.(*Element) if !ok { return fmt.Errorf("obj not a storeElement: %#v", obj) } @@ -331,7 +331,7 @@ func (i *indexer) Replace(objs []interface{}, resourceVersion string) error { return nil } -func (i *indexer) updateElem(key string, oldObj, newObj *storeElement) (err error) { +func (i *indexer) updateElem(key string, oldObj, newObj *Element) (err error) { var oldIndexValues, indexValues []string for name, indexFunc := range i.indexers { if oldObj != nil { @@ -352,7 +352,7 @@ func (i *indexer) updateElem(key string, oldObj, newObj *storeElement) (err erro } index := i.indices[name] if index == nil { - index = map[string]map[string]*storeElement{} + index = map[string]map[string]*Element{} i.indices[name] = index } if len(indexValues) == 1 && len(oldIndexValues) == 1 && indexValues[0] == oldIndexValues[0] { @@ -370,16 +370,16 @@ func (i *indexer) updateElem(key string, oldObj, newObj *storeElement) (err erro return nil } -func (i *indexer) add(key, value string, obj *storeElement, index map[string]map[string]*storeElement) { +func (i *indexer) add(key, value string, obj *Element, index map[string]map[string]*Element) { set := index[value] if set == nil { - set = map[string]*storeElement{} + set = map[string]*Element{} index[value] = set } set[key] = obj } -func (i *indexer) delete(key, value string, index map[string]map[string]*storeElement) { +func (i *indexer) delete(key, value string, index map[string]map[string]*Element) { set := index[value] if set == nil { return @@ -426,9 +426,9 @@ func (i *indexer) delete(key, value string, index map[string]map[string]*storeEl // However, this solution is more complex and is deferred for future implementation. // // TODO: Rewrite to use a cyclic buffer -func newStoreSnapshotter() *storeSnapshotter { +func NewSnapshotter() Snapshotter { s := &storeSnapshotter{ - snapshots: btree.NewG[rvSnapshot](btreeDegree, func(a, b rvSnapshot) bool { + snapshots: btree.New(btreeDegree, func(a, b rvSnapshot) bool { return a.resourceVersion < b.resourceVersion }), } @@ -439,20 +439,20 @@ var _ Snapshotter = (*storeSnapshotter)(nil) type Snapshotter interface { Reset() - GetLessOrEqual(rv uint64) (orderedLister, bool) - Add(rv uint64, indexer orderedLister) + GetLessOrEqual(rv uint64) (OrderedLister, bool) + Add(rv uint64, indexer OrderedLister) RemoveLess(rv uint64) Len() int } type storeSnapshotter struct { mux sync.RWMutex - snapshots *btree.BTreeG[rvSnapshot] + snapshots *btree.BTree[rvSnapshot] } type rvSnapshot struct { resourceVersion uint64 - snapshot orderedLister + snapshot OrderedLister } func (s *storeSnapshotter) Reset() { @@ -461,7 +461,7 @@ func (s *storeSnapshotter) Reset() { s.snapshots.Clear(false) } -func (s *storeSnapshotter) GetLessOrEqual(rv uint64) (orderedLister, bool) { +func (s *storeSnapshotter) GetLessOrEqual(rv uint64) (OrderedLister, bool) { s.mux.RLock() defer s.mux.RUnlock() @@ -476,7 +476,7 @@ func (s *storeSnapshotter) GetLessOrEqual(rv uint64) (orderedLister, bool) { return result.snapshot, true } -func (s *storeSnapshotter) Add(rv uint64, indexer orderedLister) { +func (s *storeSnapshotter) Add(rv uint64, indexer OrderedLister) { s.mux.Lock() defer s.mux.Unlock() s.snapshots.ReplaceOrInsert(rvSnapshot{resourceVersion: rv, snapshot: indexer.Clone()}) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree_test.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree_test.go index b461fe3b1..2ea923d64 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_btree_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_btree_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cacher +package store import ( "testing" @@ -71,12 +71,12 @@ func TestStoreListPrefix(t *testing.T) { } func TestStoreSnapshotter(t *testing.T) { - cache := newStoreSnapshotter() + cache := NewSnapshotter() cache.Add(10, fakeOrderedLister{rv: 10}) cache.Add(20, fakeOrderedLister{rv: 20}) cache.Add(30, fakeOrderedLister{rv: 30}) cache.Add(40, fakeOrderedLister{rv: 40}) - assert.Equal(t, 4, cache.snapshots.Len()) + assert.Equal(t, 4, cache.Len()) t.Log("No snapshot from before first RV") _, found := cache.GetLessOrEqual(9) @@ -105,7 +105,7 @@ func TestStoreSnapshotter(t *testing.T) { t.Log("Remove snapshot less than 30") cache.RemoveLess(30) - assert.Equal(t, 2, cache.snapshots.Len()) + assert.Equal(t, 2, cache.Len()) _, found = cache.GetLessOrEqual(10) assert.False(t, found) @@ -118,7 +118,7 @@ func TestStoreSnapshotter(t *testing.T) { t.Log("Remove removing all RVs") cache.Reset() - assert.Equal(t, 0, cache.snapshots.Len()) + assert.Equal(t, 0, cache.Len()) _, found = cache.GetLessOrEqual(30) assert.False(t, found) _, found = cache.GetLessOrEqual(40) @@ -132,26 +132,26 @@ type fakeOrderedLister struct { func (f fakeOrderedLister) Add(obj interface{}) error { return nil } func (f fakeOrderedLister) Update(obj interface{}) error { return nil } func (f fakeOrderedLister) Delete(obj interface{}) error { return nil } -func (f fakeOrderedLister) Clone() orderedLister { return f } +func (f fakeOrderedLister) Clone() OrderedLister { return f } func (f fakeOrderedLister) ListPrefix(prefixKey, continueKey string) []interface{} { return nil } func (f fakeOrderedLister) Count(prefixKey, continueKey string) int { return 0 } type fakeSnapshotter struct { - getLessOrEqual func(rv uint64) (orderedLister, bool) + getLessOrEqual func(rv uint64) (OrderedLister, bool) } var _ Snapshotter = (*fakeSnapshotter)(nil) func (f *fakeSnapshotter) Reset() {} -func (f *fakeSnapshotter) GetLessOrEqual(rv uint64) (orderedLister, bool) { +func (f *fakeSnapshotter) GetLessOrEqual(rv uint64) (OrderedLister, bool) { if f.getLessOrEqual == nil { return nil, false } return f.getLessOrEqual(rv) } -func (f *fakeSnapshotter) Add(rv uint64, indexer orderedLister) {} +func (f *fakeSnapshotter) Add(rv uint64, indexer OrderedLister) {} func (f *fakeSnapshotter) RemoveLess(rv uint64) {} func (f *fakeSnapshotter) Len() int { return 0 diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_test.go similarity index 83% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_test.go index 7018bc334..aeca893fc 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/store_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/store/store_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cacher +package store import ( "testing" @@ -29,16 +29,16 @@ import ( func TestStoreSingleKey(t *testing.T) { t.Run("cache.Indexer", func(t *testing.T) { - store := newStoreIndexer(testStoreIndexers()) + store := NewIndexer(testStoreIndexers()) testStoreSingleKey(t, store) }) t.Run("btree", func(t *testing.T) { - store := newThreadedBtreeStoreIndexer(storeElementIndexers(testStoreIndexers()), btreeDegree) + store := newThreadedBtreeStoreIndexer(ElementIndexers(testStoreIndexers()), btreeDegree) testStoreSingleKey(t, store) }) } -func testStoreSingleKey(t *testing.T, store storeIndexer) { +func testStoreSingleKey(t *testing.T, store Indexer) { assertStoreEmpty(t, store, "foo") require.NoError(t, store.Add(testStorageElement("foo", "bar", 1))) @@ -61,16 +61,16 @@ func testStoreSingleKey(t *testing.T, store storeIndexer) { func TestStoreIndexerSingleKey(t *testing.T) { t.Run("cache.Indexer", func(t *testing.T) { - store := newStoreIndexer(testStoreIndexers()) + store := NewIndexer(testStoreIndexers()) testStoreIndexerSingleKey(t, store) }) t.Run("btree", func(t *testing.T) { - store := newThreadedBtreeStoreIndexer(storeElementIndexers(testStoreIndexers()), btreeDegree) + store := newThreadedBtreeStoreIndexer(ElementIndexers(testStoreIndexers()), btreeDegree) testStoreIndexerSingleKey(t, store) }) } -func testStoreIndexerSingleKey(t *testing.T, store storeIndexer) { +func testStoreIndexerSingleKey(t *testing.T, store Indexer) { items, err := store.ByIndex("by_val", "bar") require.NoError(t, err) assert.Empty(t, items) @@ -122,7 +122,7 @@ func testStoreIndexerSingleKey(t *testing.T, store storeIndexer) { require.NoError(t, store.Delete(testStorageElement("foo", "", 0))) } -func assertStoreEmpty(t *testing.T, store storeIndexer, nonExistingKey string) { +func assertStoreEmpty(t *testing.T, store Indexer, nonExistingKey string) { item, ok, err := store.Get(testStorageElement(nonExistingKey, "", 0)) require.NoError(t, err) assert.False(t, ok) @@ -137,23 +137,23 @@ func assertStoreEmpty(t *testing.T, store storeIndexer, nonExistingKey string) { assert.Empty(t, items) } -func assertStoreSingleKey(t *testing.T, store storeIndexer, expectKey, expectValue string, expectRV int) { +func assertStoreSingleKey(t *testing.T, store Indexer, expectKey, expectValue string, expectRV int) { item, ok, err := store.Get(testStorageElement(expectKey, "", expectRV)) require.NoError(t, err) assert.True(t, ok) - assert.Equal(t, expectValue, item.(*storeElement).Object.(fakeObj).value) + assert.Equal(t, expectValue, item.(*Element).Object.(fakeObj).value) item, ok, err = store.GetByKey(expectKey) require.NoError(t, err) assert.True(t, ok) - assert.Equal(t, expectValue, item.(*storeElement).Object.(fakeObj).value) + assert.Equal(t, expectValue, item.(*Element).Object.(fakeObj).value) items := store.List() assert.Equal(t, []interface{}{testStorageElement(expectKey, expectValue, expectRV)}, items) } -func testStorageElement(key, value string, rv int) *storeElement { - return &storeElement{Key: key, Object: fakeObj{value: value, rv: rv}} +func testStorageElement(key, value string, rv int) *Element { + return &Element{Key: key, Object: fakeObj{value: value, rv: rv}} } type fakeObj struct { diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/testing/mock.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/testing/mock.go new file mode 100644 index 000000000..f9f445ceb --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/testing/mock.go @@ -0,0 +1,153 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testing + +import ( + "context" + "fmt" + "sync" + _ "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/apis/example" + "k8s.io/apiserver/pkg/storage" +) + +type MockStorage struct { + sync.RWMutex + GetListErr error + WatchErr error + GetListFn func(_ context.Context, _ string, _ storage.ListOptions, listObj runtime.Object) error + GetRVFn func(_ context.Context) (uint64, error) + WatchFn func(_ context.Context, _ string, _ storage.ListOptions) (watch.Interface, error) + + // use GetRequestWatchProgressCounter when reading + // the value of the counter + RequestWatchProgressCounter int +} + +func (d *MockStorage) RequestWatchProgress(ctx context.Context) error { + d.Lock() + defer d.Unlock() + d.RequestWatchProgressCounter++ + return nil +} + +func (d *MockStorage) GetRequestWatchProgressCounter() int { + d.RLock() + defer d.RUnlock() + return d.RequestWatchProgressCounter +} + +func (d *MockStorage) CompactRevision() int64 { + return 0 +} + +func (d *MockStorage) IsWatchListSemanticsUnSupported() bool { + return true +} + +type MockWatch struct { + ch chan watch.Event +} + +func (w *MockWatch) ResultChan() <-chan watch.Event { + return w.ch +} + +func (w *MockWatch) Stop() { + close(w.ch) +} + +func NewMockWatch() watch.Interface { + return &MockWatch{ + ch: make(chan watch.Event), + } +} + +func (d *MockStorage) Versioner() storage.Versioner { return nil } +func (d *MockStorage) Create(_ context.Context, _ string, _, _ runtime.Object, _ uint64) error { + return fmt.Errorf("unimplemented") +} +func (d *MockStorage) Delete(_ context.Context, _ string, _ runtime.Object, _ *storage.Preconditions, _ storage.ValidateObjectFunc, _ runtime.Object, _ storage.DeleteOptions) error { + return fmt.Errorf("unimplemented") +} +func (d *MockStorage) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { + if d.WatchFn != nil { + return d.WatchFn(ctx, key, opts) + } + d.RLock() + defer d.RUnlock() + + return NewMockWatch(), d.WatchErr +} +func (d *MockStorage) Get(_ context.Context, _ string, _ storage.GetOptions, _ runtime.Object) error { + d.RLock() + defer d.RUnlock() + return d.GetListErr +} +func (d *MockStorage) GetList(ctx context.Context, resPrefix string, opts storage.ListOptions, listObj runtime.Object) error { + if d.GetListFn != nil { + return d.GetListFn(ctx, resPrefix, opts, listObj) + } + d.RLock() + defer d.RUnlock() + podList := listObj.(*example.PodList) + podList.ListMeta = metav1.ListMeta{ResourceVersion: "100"} + return d.GetListErr +} +func (d *MockStorage) GuaranteedUpdate(_ context.Context, _ string, _ runtime.Object, _ bool, _ *storage.Preconditions, _ storage.UpdateFunc, _ runtime.Object) error { + return fmt.Errorf("unimplemented") +} +func (d *MockStorage) Stats(_ context.Context) (storage.Stats, error) { + return storage.Stats{}, fmt.Errorf("unimplemented") +} +func (d *MockStorage) EnableResourceSizeEstimation(storage.KeysFunc) error { + return nil +} +func (d *MockStorage) ReadinessCheck() error { + return nil +} +func (d *MockStorage) InjectGetListError(err error) { + d.Lock() + defer d.Unlock() + + d.GetListErr = err +} + +func (d *MockStorage) GetCurrentResourceVersion(ctx context.Context) (uint64, error) { + if d.GetRVFn != nil { + return d.GetRVFn(ctx) + } + return 100, nil +} + +type MockCacher struct { + MockStorage + IsReady bool + Consistent bool +} + +func (d *MockCacher) Ready() bool { + return d.IsReady +} + +func (d *MockCacher) MarkConsistent(consistent bool) { + d.Consistent = consistent +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/time_budget.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/time_budget.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/time_budget.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/time_budget.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/time_budget_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/time_budget_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/time_budget_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/time_budget_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/util.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/util.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/util_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/util_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/util_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/util_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache.go index 620b0f36b..fc73708a8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache.go @@ -37,6 +37,7 @@ import ( "k8s.io/apiserver/pkg/storage/cacher/delegator" "k8s.io/apiserver/pkg/storage/cacher/metrics" "k8s.io/apiserver/pkg/storage/cacher/progress" + "k8s.io/apiserver/pkg/storage/cacher/store" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" "k8s.io/component-base/tracing" @@ -121,7 +122,7 @@ type watchCache struct { // history" i.e. from the moment just after the newest cached watched event. // It is necessary to effectively allow clients to start watching at now. // NOTE: We assume that is thread-safe. - store storeIndexer + store store.Indexer // ResourceVersion up to which the watchCache is propagated. resourceVersion uint64 @@ -156,7 +157,7 @@ type watchCache struct { waitingUntilFresh *progress.ConditionalProgressRequester // Stores previous snapshots of orderedLister to allow serving requests from previous revisions. - snapshots Snapshotter + snapshots store.Snapshotter snapshottingEnabled atomic.Bool getCurrentRV func(context.Context) (uint64, error) @@ -183,7 +184,7 @@ func newWatchCache( upperBoundCapacity: capacityUpperBound(eventFreshDuration), startIndex: 0, endIndex: 0, - store: newStoreIndexer(indexers), + store: store.NewIndexer(indexers), resourceVersion: 0, listResourceVersion: 0, eventHandler: eventHandler, @@ -196,7 +197,7 @@ func newWatchCache( } if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { wc.snapshottingEnabled.Store(true) - wc.snapshots = newStoreSnapshotter() + wc.snapshots = store.NewSnapshotter() } metrics.WatchCacheCapacity.WithLabelValues(groupResource.Group, groupResource.Resource).Set(float64(wc.capacity)) wc.cond = sync.NewCond(wc.RLocker()) @@ -237,7 +238,7 @@ func (w *watchCache) Add(obj interface{}) error { } event := watch.Event{Type: watch.Added, Object: object} - f := func(elem *storeElement) error { return w.store.Add(elem) } + f := func(elem *store.Element) error { return w.store.Add(elem) } return w.processEvent(event, resourceVersion, f) } @@ -249,7 +250,7 @@ func (w *watchCache) Update(obj interface{}) error { } event := watch.Event{Type: watch.Modified, Object: object} - f := func(elem *storeElement) error { return w.store.Update(elem) } + f := func(elem *store.Element) error { return w.store.Update(elem) } return w.processEvent(event, resourceVersion, f) } @@ -261,7 +262,7 @@ func (w *watchCache) Delete(obj interface{}) error { } event := watch.Event{Type: watch.Deleted, Object: object} - f := func(elem *storeElement) error { return w.store.Delete(elem) } + f := func(elem *store.Element) error { return w.store.Delete(elem) } return w.processEvent(event, resourceVersion, f) } @@ -279,14 +280,14 @@ func (w *watchCache) objectToVersionedRuntimeObject(obj interface{}) (runtime.Ob // processEvent is safe as long as there is at most one call to it in flight // at any point in time. -func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, updateFunc func(*storeElement) error) error { +func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, updateFunc func(*store.Element) error) error { metrics.EventsReceivedCounter.WithLabelValues(w.groupResource.Group, w.groupResource.Resource).Inc() key, err := w.keyFunc(event.Object) if err != nil { return fmt.Errorf("couldn't compute key: %v", err) } - elem := &storeElement{Key: key, Object: event.Object} + elem := &store.Element{Key: key, Object: event.Object} elem.Labels, elem.Fields, err = w.getAttrsFunc(event.Object) if err != nil { return err @@ -312,7 +313,7 @@ func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, upd return err } if exists { - previousElem := previous.(*storeElement) + previousElem := previous.(*store.Element) wcEvent.PrevObject = previousElem.Object wcEvent.PrevObjLabels = previousElem.Labels wcEvent.PrevObjFields = previousElem.Fields @@ -331,7 +332,7 @@ func (w *watchCache) processEvent(event watch.Event, resourceVersion uint64, upd return err } if w.snapshots != nil && w.snapshottingEnabled.Load() { - if orderedLister, ordered := w.store.(orderedLister); ordered { + if orderedLister, ordered := w.store.(store.OrderedLister); ordered { if w.isCacheFullLocked() { oldestRV := w.cache[w.startIndex%w.capacity].ResourceVersion w.snapshots.RemoveLess(oldestRV) @@ -437,7 +438,7 @@ func (w *watchCache) UpdateResourceVersion(resourceVersion string) { metrics.RecordResourceVersion(w.groupResource, rv) } -// List returns list of pointers to objects. +// List returns list of pointers to objects. func (w *watchCache) List() []interface{} { return w.store.List() } @@ -495,7 +496,7 @@ func (s sortableStoreElements) Len() int { } func (s sortableStoreElements) Less(i, j int) bool { - return s[i].(*storeElement).Key < s[j].(*storeElement).Key + return s[i].(*store.Element).Key < s[j].(*store.Element).Key } func (s sortableStoreElements) Swap(i, j int) { @@ -652,7 +653,7 @@ func (w *watchCache) listLatestRV(key, continueKey string, matchValues []storage }, matchValue.IndexName, err } } - if store, ok := w.store.(orderedLister); ok { + if store, ok := w.store.(store.OrderedLister); ok { result := store.ListPrefix(key, continueKey) return listResp{ Items: result, @@ -670,9 +671,9 @@ func (w *watchCache) listLatestRV(key, continueKey string, matchValues []storage func filterPrefixAndOrder(prefix string, items []interface{}) ([]interface{}, error) { var result []interface{} for _, item := range items { - elem, ok := item.(*storeElement) + elem, ok := item.(*store.Element) if !ok { - return nil, fmt.Errorf("non *storeElement returned from storage: %v", item) + return nil, fmt.Errorf("non *store.Element returned from storage: %v", item) } if !hasPathPrefix(elem.Key, prefix) { continue @@ -723,7 +724,7 @@ func (w *watchCache) Get(obj interface{}) (interface{}, bool, error) { return nil, false, fmt.Errorf("couldn't compute key: %v", err) } - return w.store.Get(&storeElement{Key: key, Object: object}) + return w.store.Get(&store.Element{Key: key, Object: object}) } // GetByKey returns pointer to . @@ -752,7 +753,7 @@ func (w *watchCache) Replace(objs []interface{}, resourceVersion string) error { if err != nil { return err } - toReplace = append(toReplace, &storeElement{ + toReplace = append(toReplace, &store.Element{ Key: key, Object: object, Labels: objLabels, @@ -777,7 +778,7 @@ func (w *watchCache) Replace(objs []interface{}, resourceVersion string) error { } if w.snapshots != nil { w.snapshots.Reset() - if orderedLister, ordered := w.store.(orderedLister); ordered && w.snapshottingEnabled.Load() { + if orderedLister, ordered := w.store.(store.OrderedLister); ordered && w.snapshottingEnabled.Load() { w.snapshots.Add(version, orderedLister) } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval.go index b74b3ca91..8cdcac370 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval.go @@ -22,6 +22,7 @@ import ( "sync" "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/storage/cacher/store" ) // watchCacheInterval serves as an abstraction over a source @@ -136,11 +137,11 @@ func (s sortableWatchCacheEvents) Swap(i, j int) { // returned by Next() need to be events from a List() done on the underlying store of // the watch cache. // The items returned in the interval will be sorted by Key. -func newCacheIntervalFromStore(resourceVersion uint64, store storeIndexer, key string, matchesSingle bool) (*watchCacheInterval, error) { +func newCacheIntervalFromStore(resourceVersion uint64, indexer store.Indexer, key string, matchesSingle bool) (*watchCacheInterval, error) { buffer := &watchCacheIntervalBuffer{} var allItems []interface{} if matchesSingle { - item, exists, err := store.GetByKey(key) + item, exists, err := indexer.GetByKey(key) if err != nil { return nil, err } @@ -149,11 +150,11 @@ func newCacheIntervalFromStore(resourceVersion uint64, store storeIndexer, key s allItems = append(allItems, item) } } else { - allItems = store.List() + allItems = indexer.List() } buffer.buffer = make([]*watchCacheEvent, len(allItems)) for i, item := range allItems { - elem, ok := item.(*storeElement) + elem, ok := item.(*store.Element) if !ok { return nil, fmt.Errorf("not a storeElement: %v", elem) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval_test.go index 21dec3500..f201f06a3 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_interval_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_interval_test.go @@ -29,6 +29,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" + + "k8s.io/apiserver/pkg/storage/cacher/store" ) func intervalFromEvents(events []*watchCacheEvent) *watchCacheInterval { @@ -371,7 +373,7 @@ func TestCacheIntervalNextFromStore(t *testing.T) { return labels.Set(pod.Labels), fields.Set{"spec.nodeName": pod.Spec.NodeName}, nil } const numEvents = 50 - store := cache.NewIndexer(storeElementKey, storeElementIndexers(nil)) + store := cache.NewIndexer(store.ElementKey, store.ElementIndexers(nil)) events := make(map[string]*watchCacheEvent) var rv uint64 = 1 // arbitrary number; rv till which the watch cache has progressed. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_test.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_test.go index 520bdcdfd..548bf0f94 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/cacher/watch_cache_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/cacher/watch_cache_test.go @@ -42,6 +42,7 @@ import ( "k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage/cacher/metrics" "k8s.io/apiserver/pkg/storage/cacher/progress" + "k8s.io/apiserver/pkg/storage/cacher/store" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/tools/cache" featuregatetesting "k8s.io/component-base/featuregate/testing" @@ -69,9 +70,9 @@ func makeTestPodDetails(name string, resourceVersion uint64, nodeName string, la } } -func makeTestStoreElement(pod *v1.Pod) *storeElement { - return &storeElement{ - Key: "prefix/ns/" + pod.Name, +func makeTestStoreElement(pod *v1.Pod) *store.Element { + return &store.Element{ + Key: "/prefix/ns/" + pod.Name, Object: pod, Labels: labels.Set(pod.Labels), Fields: fields.Set{"spec.nodeName": pod.Spec.NodeName}, @@ -116,7 +117,7 @@ func (w *testWatchCache) getCacheIntervalForEvents(resourceVersion uint64, opts // newTestWatchCache just adds a fake clock. func newTestWatchCache(capacity int, eventFreshDuration time.Duration, indexers *cache.Indexers) *testWatchCache { keyFunc := func(obj runtime.Object) (string, error) { - return storage.NamespaceKeyFunc("prefix", obj) + return storage.NamespaceKeyFunc("/prefix/", obj) } getAttrsFunc := func(obj runtime.Object) (labels.Set, fields.Set, error) { pod, ok := obj.(*v1.Pod) @@ -202,15 +203,15 @@ func (w *testWatchCache) Stop() { } func TestWatchCacheBasic(t *testing.T) { - store := newTestWatchCache(2, DefaultEventFreshDuration, &cache.Indexers{}) - defer store.Stop() + s := newTestWatchCache(2, DefaultEventFreshDuration, &cache.Indexers{}) + defer s.Stop() // Test Add/Update/Delete. pod1 := makeTestPod("pod", 1) - if err := store.Add(pod1); err != nil { + if err := s.Add(pod1); err != nil { t.Errorf("unexpected error: %v", err) } - if item, ok, _ := store.Get(pod1); !ok { + if item, ok, _ := s.Get(pod1); !ok { t.Errorf("didn't find pod") } else { expected := makeTestStoreElement(makeTestPod("pod", 1)) @@ -219,10 +220,10 @@ func TestWatchCacheBasic(t *testing.T) { } } pod2 := makeTestPod("pod", 2) - if err := store.Update(pod2); err != nil { + if err := s.Update(pod2); err != nil { t.Errorf("unexpected error: %v", err) } - if item, ok, _ := store.Get(pod2); !ok { + if item, ok, _ := s.Get(pod2); !ok { t.Errorf("didn't find pod") } else { expected := makeTestStoreElement(makeTestPod("pod", 2)) @@ -231,26 +232,26 @@ func TestWatchCacheBasic(t *testing.T) { } } pod3 := makeTestPod("pod", 3) - if err := store.Delete(pod3); err != nil { + if err := s.Delete(pod3); err != nil { t.Errorf("unexpected error: %v", err) } - if _, ok, _ := store.Get(pod3); ok { + if _, ok, _ := s.Get(pod3); ok { t.Errorf("found pod") } // Test List. - store.Add(makeTestPod("pod1", 4)) - store.Add(makeTestPod("pod2", 5)) - store.Add(makeTestPod("pod3", 6)) + s.Add(makeTestPod("pod1", 4)) + s.Add(makeTestPod("pod2", 5)) + s.Add(makeTestPod("pod3", 6)) { - expected := map[string]storeElement{ - "prefix/ns/pod1": *makeTestStoreElement(makeTestPod("pod1", 4)), - "prefix/ns/pod2": *makeTestStoreElement(makeTestPod("pod2", 5)), - "prefix/ns/pod3": *makeTestStoreElement(makeTestPod("pod3", 6)), + expected := map[string]store.Element{ + "/prefix/ns/pod1": *makeTestStoreElement(makeTestPod("pod1", 4)), + "/prefix/ns/pod2": *makeTestStoreElement(makeTestPod("pod2", 5)), + "/prefix/ns/pod3": *makeTestStoreElement(makeTestPod("pod3", 6)), } - items := make(map[string]storeElement) - for _, item := range store.List() { - elem := item.(*storeElement) + items := make(map[string]store.Element) + for _, item := range s.List() { + elem := item.(*store.Element) items[elem.Key] = *elem } if !apiequality.Semantic.DeepEqual(expected, items) { @@ -259,18 +260,18 @@ func TestWatchCacheBasic(t *testing.T) { } // Test Replace. - store.Replace([]interface{}{ + s.Replace([]interface{}{ makeTestPod("pod4", 7), makeTestPod("pod5", 8), }, "8") { - expected := map[string]storeElement{ - "prefix/ns/pod4": *makeTestStoreElement(makeTestPod("pod4", 7)), - "prefix/ns/pod5": *makeTestStoreElement(makeTestPod("pod5", 8)), + expected := map[string]store.Element{ + "/prefix/ns/pod4": *makeTestStoreElement(makeTestPod("pod4", 7)), + "/prefix/ns/pod5": *makeTestStoreElement(makeTestPod("pod5", 8)), } - items := make(map[string]storeElement) - for _, item := range store.List() { - elem := item.(*storeElement) + items := make(map[string]store.Element) + for _, item := range s.List() { + elem := item.(*store.Element) items[elem.Key] = *elem } if !apiequality.Semantic.DeepEqual(expected, items) { @@ -470,7 +471,7 @@ func TestWaitUntilFreshAndGetList(t *testing.T) { }() // list by empty MatchValues. - resp, indexUsed, err := store.WaitUntilFreshAndGetList(ctx, "prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.Everything}) + resp, indexUsed, err := store.WaitUntilFreshAndGetList(ctx, "/prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.Everything}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -485,7 +486,7 @@ func TestWaitUntilFreshAndGetList(t *testing.T) { } // list by label index. - resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ + resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "/prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ Label: labels.SelectorFromSet(map[string]string{ "label": "value1", }), @@ -508,7 +509,7 @@ func TestWaitUntilFreshAndGetList(t *testing.T) { } // list with spec.nodeName index. - resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ + resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "/prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ Label: labels.SelectorFromSet(map[string]string{ "not-exist-label": "whatever", }), @@ -531,7 +532,7 @@ func TestWaitUntilFreshAndGetList(t *testing.T) { } // list with index not exists. - resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ + resp, indexUsed, err = store.WaitUntilFreshAndGetList(ctx, "/prefix/", storage.ListOptions{ResourceVersion: "5", Recursive: true, Predicate: storage.SelectionPredicate{ Label: labels.SelectorFromSet(map[string]string{ "not-exist-label": "whatever", }), @@ -565,7 +566,7 @@ func TestWaitUntilFreshAndListFromCache(t *testing.T) { }() // list from future revision. Requires watch cache to request bookmark to get it. - resp, indexUsed, err := store.WaitUntilFreshAndGetList(ctx, "prefix/", storage.ListOptions{ResourceVersion: "3", Recursive: true, Predicate: storage.Everything}) + resp, indexUsed, err := store.WaitUntilFreshAndGetList(ctx, "/prefix/", storage.ListOptions{ResourceVersion: "3", Recursive: true, Predicate: storage.Everything}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -591,7 +592,7 @@ func TestWaitUntilFreshAndGet(t *testing.T) { store.Add(makeTestPod("bar", 5)) }() - obj, exists, resourceVersion, err := store.WaitUntilFreshAndGet(ctx, 5, "prefix/ns/bar") + obj, exists, resourceVersion, err := store.WaitUntilFreshAndGet(ctx, 5, "/prefix/ns/bar") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1316,97 +1317,97 @@ func TestHistogramCacheReadWait(t *testing.T) { func TestCacheSnapshots(t *testing.T) { featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ListFromCacheSnapshot, true) - store := newTestWatchCache(3, DefaultEventFreshDuration, &cache.Indexers{}) - defer store.Stop() - store.upperBoundCapacity = 3 - store.lowerBoundCapacity = 1 - clock := store.clock.(*testingclock.FakeClock) + s := newTestWatchCache(3, DefaultEventFreshDuration, &cache.Indexers{}) + defer s.Stop() + s.upperBoundCapacity = 3 + s.lowerBoundCapacity = 1 + clock := s.clock.(*testingclock.FakeClock) - _, found := store.snapshots.GetLessOrEqual(100) + _, found := s.snapshots.GetLessOrEqual(100) assert.False(t, found, "Expected empty cache to not include any snapshots") t.Log("Test cache on rev 100") - require.NoError(t, store.Add(makeTestPod("foo", 100))) - require.NoError(t, store.Update(makeTestPod("foo", 200))) + require.NoError(t, s.Add(makeTestPod("foo", 100))) + require.NoError(t, s.Update(makeTestPod("foo", 200))) clock.Step(time.Second) - require.NoError(t, store.Delete(makeTestPod("foo", 300))) + require.NoError(t, s.Delete(makeTestPod("foo", 300))) t.Log("Test cache on rev 100") - _, found = store.snapshots.GetLessOrEqual(99) + _, found = s.snapshots.GetLessOrEqual(99) assert.False(t, found, "Expected store to not include rev 99") - lister, found := store.snapshots.GetLessOrEqual(100) + lister, found := s.snapshots.GetLessOrEqual(100) assert.True(t, found, "Expected store to not include rev 100") elements := lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 100), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 100), elements[0].(*store.Element).Object) t.Log("Overflow cache to remove rev 100") - require.NoError(t, store.Add(makeTestPod("foo", 400))) - _, found = store.snapshots.GetLessOrEqual(100) + require.NoError(t, s.Add(makeTestPod("foo", 400))) + _, found = s.snapshots.GetLessOrEqual(100) assert.False(t, found, "Expected overfilled cache to delete oldest rev 100") t.Log("Test cache on rev 200") - lister, found = store.snapshots.GetLessOrEqual(200) + lister, found = s.snapshots.GetLessOrEqual(200) assert.True(t, found, "Expected store to still keep rev 200") elements = lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 200), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 200), elements[0].(*store.Element).Object) t.Log("Test cache on rev 300") - lister, found = store.snapshots.GetLessOrEqual(300) + lister, found = s.snapshots.GetLessOrEqual(300) assert.True(t, found, "Expected store to still keep rev 300") elements = lister.ListPrefix("", "") assert.Empty(t, elements) t.Log("Test cache on rev 400") - lister, found = store.snapshots.GetLessOrEqual(400) + lister, found = s.snapshots.GetLessOrEqual(400) assert.True(t, found, "Expected store to still keep rev 400") elements = lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 400), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 400), elements[0].(*store.Element).Object) t.Log("Add event outside the event fresh window to force cache capacity downsize") - assert.Equal(t, 3, store.capacity) + assert.Equal(t, 3, s.capacity) clock.Step(DefaultEventFreshDuration + 1) - require.NoError(t, store.Update(makeTestPod("foo", 500))) - assert.Equal(t, 1, store.capacity) - assert.Equal(t, 1, store.snapshots.Len()) - _, found = store.snapshots.GetLessOrEqual(499) + require.NoError(t, s.Update(makeTestPod("foo", 500))) + assert.Equal(t, 1, s.capacity) + assert.Equal(t, 1, s.snapshots.Len()) + _, found = s.snapshots.GetLessOrEqual(499) assert.False(t, found, "Expected overfilled cache to delete events below 500") t.Log("Test cache on rev 500") - lister, found = store.snapshots.GetLessOrEqual(500) + lister, found = s.snapshots.GetLessOrEqual(500) assert.True(t, found, "Expected store to still keep rev 500") elements = lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 500), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 500), elements[0].(*store.Element).Object) t.Log("Add event to force capacity upsize") - require.NoError(t, store.Update(makeTestPod("foo", 600))) - assert.Equal(t, 2, store.capacity) - assert.Equal(t, 2, store.snapshots.Len()) + require.NoError(t, s.Update(makeTestPod("foo", 600))) + assert.Equal(t, 2, s.capacity) + assert.Equal(t, 2, s.snapshots.Len()) t.Log("Test cache on rev 600") - lister, found = store.snapshots.GetLessOrEqual(600) + lister, found = s.snapshots.GetLessOrEqual(600) assert.True(t, found, "Expected replace to be snapshotted") elements = lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 600), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 600), elements[0].(*store.Element).Object) t.Log("Replace cache to remove history") - _, found = store.snapshots.GetLessOrEqual(500) + _, found = s.snapshots.GetLessOrEqual(500) assert.True(t, found, "Confirm that cache stores history before replace") - err := store.Replace([]interface{}{makeTestPod("foo", 600)}, "700") + err := s.Replace([]interface{}{makeTestPod("foo", 600)}, "700") require.NoError(t, err) - _, found = store.snapshots.GetLessOrEqual(500) + _, found = s.snapshots.GetLessOrEqual(500) assert.False(t, found, "Expected replace to remove history") - _, found = store.snapshots.GetLessOrEqual(600) + _, found = s.snapshots.GetLessOrEqual(600) assert.False(t, found, "Expected replace to remove history") t.Log("Test cache on rev 700") - lister, found = store.snapshots.GetLessOrEqual(700) + lister, found = s.snapshots.GetLessOrEqual(700) assert.True(t, found, "Expected replace to be snapshotted") elements = lister.ListPrefix("", "") assert.Len(t, elements, 1) - assert.Equal(t, makeTestPod("foo", 600), elements[0].(*storeElement).Object) + assert.Equal(t, makeTestPod("foo", 600), elements[0].(*store.Element).Object) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/continue.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/continue.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/continue.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/continue.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/continue_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/continue_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/continue_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/continue_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors/storage.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors/storage.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/errors/storage.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/errors/storage.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/compact.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/compact.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/compact.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/compact.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/compact_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/compact_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/compact_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/compact_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/corrupt_obj_deleter.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/corrupt_obj_deleter.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/corrupt_obj_deleter.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/corrupt_obj_deleter.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/decoder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/decoder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/decoder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/decoder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/errors.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/errors.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/errors.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/errors.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/event.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/event.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/event.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/event.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/event_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/event_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/event_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/event_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/healthcheck.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/healthcheck.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/healthcheck.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/healthcheck.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/healthcheck_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/healthcheck_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/healthcheck_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/healthcheck_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/latency_tracker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/latency_tracker.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/latency_tracker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/latency_tracker.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/lease_manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/lease_manager.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/lease_manager.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/lease_manager.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/lease_manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/lease_manager_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/lease_manager_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/lease_manager_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/linearized_read_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/linearized_read_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/linearized_read_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/linearized_read_test.go index 7331c8245..879647977 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/linearized_read_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/linearized_read_test.go @@ -37,7 +37,7 @@ func TestLinearizedReadRevisionInvariant(t *testing.T) { // [1] https://etcd.io/docs/v3.5/learning/api_guarantees/#isolation-level-and-consistency-of-replicas ctx, store, etcdClient := testSetup(t) - dir := "/testing" + dir := "/pods/" key := dir + "/testkey" out := &example.Pod{} obj := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", SelfLink: "testlink"}} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/logger.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/logger.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/logger.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/logger.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics.go similarity index 92% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics.go index 21eb18285..6bfec8711 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics.go @@ -72,9 +72,10 @@ var ( ) objectCounts = compbasemetrics.NewGaugeVec( &compbasemetrics.GaugeOpts{ - Name: "apiserver_storage_objects", - Help: "[DEPRECATED, consider using apiserver_resource_objects instead] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1.", - StabilityLevel: compbasemetrics.STABLE, + Name: "apiserver_storage_objects", + Help: "[DEPRECATED, consider using apiserver_resource_objects instead] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1.", + StabilityLevel: compbasemetrics.STABLE, + DeprecatedVersion: "1.34.0", }, []string{"resource"}, ) @@ -111,13 +112,22 @@ var ( Subsystem: "apiserver", Name: "storage_events_received_total", Help: "Number of etcd events received split by kind.", - StabilityLevel: compbasemetrics.ALPHA, + StabilityLevel: compbasemetrics.BETA, }, []string{"group", "resource"}, ) etcdBookmarkCounts = compbasemetrics.NewGaugeVec( &compbasemetrics.GaugeOpts{ - Name: "etcd_bookmark_counts", + Name: "etcd_bookmark_counts", + Help: "Number of etcd bookmarks (progress notify events) split by kind.", + StabilityLevel: compbasemetrics.ALPHA, + DeprecatedVersion: "1.36.0", + }, + []string{"group", "resource"}, + ) + etcdBookmarkTotal = compbasemetrics.NewCounterVec( + &compbasemetrics.CounterOpts{ + Name: "etcd_bookmark_total", Help: "Number of etcd bookmarks (progress notify events) split by kind.", StabilityLevel: compbasemetrics.ALPHA, }, @@ -191,6 +201,7 @@ func Register() { legacyregistry.CustomMustRegister(storageMonitor) legacyregistry.MustRegister(etcdEventsReceivedCounts) legacyregistry.MustRegister(etcdBookmarkCounts) + legacyregistry.MustRegister(etcdBookmarkTotal) legacyregistry.MustRegister(etcdLeaseObjectCounts) legacyregistry.MustRegister(listStorageCount) legacyregistry.MustRegister(listStorageNumFetched) @@ -245,9 +256,10 @@ func RecordEtcdEvent(groupResource schema.GroupResource) { etcdEventsReceivedCounts.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() } -// RecordEtcdBookmark updates the etcd_bookmark_counts metric. +// RecordEtcdBookmark updates the etcd_bookmark_total metric. func RecordEtcdBookmark(groupResource schema.GroupResource) { etcdBookmarkCounts.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() + etcdBookmarkTotal.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() } // RecordDecodeError sets the storage_decode_errors metrics. @@ -285,7 +297,7 @@ func UpdateLeaseObjectCount(count int64) { etcdLeaseObjectCounts.WithLabelValues().Observe(float64(count)) } -// RecordListEtcd3Metrics notes various metrics of the cost to serve a LIST request +// RecordStorageListMetrics notes various metrics of the cost to serve a LIST request func RecordStorageListMetrics(groupResource schema.GroupResource, numFetched, numEvald, numReturned int) { listStorageCount.WithLabelValues(groupResource.Group, groupResource.Resource).Inc() listStorageNumFetched.WithLabelValues(groupResource.Group, groupResource.Resource).Add(float64(numFetched)) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics_test.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics_test.go index 200739b1b..ea551745b 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/metrics/metrics_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/metrics/metrics_test.go @@ -60,6 +60,69 @@ func TestRecordDecodeError(t *testing.T) { } } +func TestRecordEtcdEvent(t *testing.T) { + registry := metrics.NewKubeRegistry() + defer registry.Reset() + registry.MustRegister(etcdEventsReceivedCounts) + testedMetrics := "apiserver_storage_events_received_total" + testCases := []struct { + desc string + resource schema.GroupResource + want string + }{ + { + desc: "record single event", + resource: schema.GroupResource{Group: "apps", Resource: "deployments"}, + want: `# HELP apiserver_storage_events_received_total [BETA] Number of etcd events received split by kind. +# TYPE apiserver_storage_events_received_total counter +apiserver_storage_events_received_total{group="apps",resource="deployments"} 1 +`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + RecordEtcdEvent(test.resource) + if err := testutil.GatherAndCompare(registry, strings.NewReader(test.want), testedMetrics); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestRecordEtcdBookmark(t *testing.T) { + registry := metrics.NewKubeRegistry() + registry.MustRegister(etcdBookmarkTotal) + + testCases := []struct { + desc string + groupResource schema.GroupResource + callCount int + want string + }{ + { + desc: "test success", + groupResource: schema.GroupResource{Group: "apps", Resource: "deployments"}, + callCount: 1, + want: `# HELP etcd_bookmark_total [ALPHA] Number of etcd bookmarks (progress notify events) split by kind. +# TYPE etcd_bookmark_total counter +etcd_bookmark_total{group="apps",resource="deployments"} 1 +`, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + for i := 0; i < test.callCount; i++ { + RecordEtcdBookmark(test.groupResource) + } + if err := testutil.GatherAndCompare(registry, strings.NewReader(test.want), "etcd_bookmark_total"); err != nil { + t.Fatal(err) + } + }) + } +} + func TestRecordEtcdRequest(t *testing.T) { registry := metrics.NewKubeRegistry() @@ -316,6 +379,7 @@ apiserver_resource_size_estimate_bytes{group="foo",resource="bar"} -1 func TestDeleteStoreStats(t *testing.T) { registry := metrics.NewKubeRegistry() registry.MustRegister(objectCounts) + registry.MustRegister(newObjectCounts) registry.MustRegister(resourceSizeEstimate) UpdateStoreStats(schema.GroupResource{Group: "foo1", Resource: "bar1"}, storage.Stats{ObjectCount: 10}, nil) @@ -325,12 +389,16 @@ func TestDeleteStoreStats(t *testing.T) { # TYPE apiserver_resource_size_estimate_bytes gauge apiserver_resource_size_estimate_bytes{group="foo1",resource="bar1"} -1 apiserver_resource_size_estimate_bytes{group="foo2",resource="bar2"} 200 +# HELP apiserver_resource_objects [ALPHA] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1. +# TYPE apiserver_resource_objects gauge +apiserver_resource_objects{group="foo1",resource="bar1"} 10 +apiserver_resource_objects{group="foo2",resource="bar2"} 20 # HELP apiserver_storage_objects [STABLE] [DEPRECATED, consider using apiserver_resource_objects instead] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1. # TYPE apiserver_storage_objects gauge apiserver_storage_objects{resource="bar1.foo1"} 10 apiserver_storage_objects{resource="bar2.foo2"} 20 ` - if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_size_estimate_bytes"); err != nil { + if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_objects", "apiserver_resource_size_estimate_bytes"); err != nil { t.Fatal(err) } @@ -339,19 +407,24 @@ apiserver_storage_objects{resource="bar2.foo2"} 20 expectedMetrics = `# HELP apiserver_resource_size_estimate_bytes [ALPHA] Estimated size of stored objects in database. Estimate is based on sum of last observed sizes of serialized objects. In case of a fetching error, the value will be -1. # TYPE apiserver_resource_size_estimate_bytes gauge apiserver_resource_size_estimate_bytes{group="foo2",resource="bar2"} 200 +# HELP apiserver_resource_objects [ALPHA] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1. +# TYPE apiserver_resource_objects gauge +apiserver_resource_objects{group="foo2",resource="bar2"} 20 # HELP apiserver_storage_objects [STABLE] [DEPRECATED, consider using apiserver_resource_objects instead] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1. # TYPE apiserver_storage_objects gauge apiserver_storage_objects{resource="bar2.foo2"} 20 ` - if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_size_estimate_bytes"); err != nil { + if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_objects", "apiserver_resource_size_estimate_bytes"); err != nil { t.Fatal(err) } DeleteStoreStats(schema.GroupResource{Group: "foo2", Resource: "bar2"}) expectedMetrics = `# HELP apiserver_storage_objects [STABLE] [DEPRECATED, consider using apiserver_resource_objects instead] Number of stored objects at the time of last check split by kind. In case of a fetching error, the value will be -1. # TYPE apiserver_storage_objects gauge +# HELP apiserver_resource_size_estimate_bytes [ALPHA] Estimated size of stored objects in database. Estimate is based on sum of last observed sizes of serialized objects. In case of a fetching error, the value will be -1. +# TYPE apiserver_resource_size_estimate_bytes gauge ` - if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_size_estimate_bytes"); err != nil { + if err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), "apiserver_storage_objects", "apiserver_resource_objects", "apiserver_resource_size_estimate_bytes"); err != nil { t.Fatal(err) } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/preflight/checks.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/preflight/checks.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/preflight/checks.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/preflight/checks.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/preflight/checks_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/preflight/checks_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/preflight/checks_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/preflight/checks_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats.go similarity index 72% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats.go index 9f0b3f78e..dcddab373 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats.go @@ -18,7 +18,6 @@ package etcd3 import ( "context" - "errors" "strings" "sync" @@ -34,11 +33,11 @@ import ( const sizerRefreshInterval = time.Minute -func newStatsCache(prefix string, getKeys storage.KeysFunc) *statsCache { +func newResourceSizeEstimator(prefix string, getKeys storage.KeysFunc) *resourceSizeEstimator { if prefix[len(prefix)-1] != '/' { prefix += "/" } - sc := &statsCache{ + sc := &resourceSizeEstimator{ prefix: prefix, getKeys: getKeys, stop: make(chan struct{}), @@ -52,23 +51,21 @@ func newStatsCache(prefix string, getKeys storage.KeysFunc) *statsCache { return sc } -// statsCache efficiently estimates the average object size +// resourceSizeEstimator efficiently estimates the average object size // based on the last observed state of individual keys. -// By plugging statsCache into GetList and Watch functions, +// By plugging resourceSizeEstimator into GetList and Watch functions, // a fairly accurate estimate of object sizes can be maintained // without additional requests to the underlying storage. // To handle potential out-of-order or incomplete data, // it uses a per-key revision to identify the newer state. // This approach may leak keys if delete events are not observed, // thus we run a background goroutine to periodically cleanup keys if needed. -type statsCache struct { +type resourceSizeEstimator struct { prefix string stop chan struct{} wg sync.WaitGroup lastKeyCleanup atomic.Pointer[time.Time] - - getKeysLock sync.Mutex - getKeys storage.KeysFunc + getKeys storage.KeysFunc keysLock sync.Mutex keys map[string]sizeRevision @@ -79,10 +76,8 @@ type sizeRevision struct { revision int64 } -var errStatsDisabled = errors.New("key size stats disabled") - -func (sc *statsCache) Stats(ctx context.Context) (storage.Stats, error) { - keys, err := sc.GetKeys(ctx) +func (sc *resourceSizeEstimator) Stats(ctx context.Context) (storage.Stats, error) { + keys, err := sc.getKeys(ctx) if err != nil { return storage.Stats{}, err } @@ -98,30 +93,12 @@ func (sc *statsCache) Stats(ctx context.Context) (storage.Stats, error) { return stats, nil } -func (sc *statsCache) GetKeys(ctx context.Context) ([]string, error) { - sc.getKeysLock.Lock() - getKeys := sc.getKeys - sc.getKeysLock.Unlock() - - if getKeys == nil { - return nil, errStatsDisabled - } - // Don't execute getKeys under lock. - return getKeys(ctx) -} - -func (sc *statsCache) SetKeysFunc(keys storage.KeysFunc) { - sc.getKeysLock.Lock() - defer sc.getKeysLock.Unlock() - sc.getKeys = keys -} - -func (sc *statsCache) Close() { +func (sc *resourceSizeEstimator) Close() { close(sc.stop) sc.wg.Wait() } -func (sc *statsCache) run() { +func (sc *resourceSizeEstimator) run() { jitter := 0.5 // Period between [interval, interval * (1.0 + jitter)] sliding := true // wait.JitterUntilWithContext starts work immediately, so wait first. @@ -132,16 +109,14 @@ func (sc *statsCache) run() { wait.JitterUntilWithContext(wait.ContextForChannel(sc.stop), sc.cleanKeysIfNeeded, sizerRefreshInterval, jitter, sliding) } -func (sc *statsCache) cleanKeysIfNeeded(ctx context.Context) { +func (sc *resourceSizeEstimator) cleanKeysIfNeeded(ctx context.Context) { lastKeyCleanup := sc.lastKeyCleanup.Load() if lastKeyCleanup != nil && time.Since(*lastKeyCleanup) < sizerRefreshInterval { return } - keys, err := sc.GetKeys(ctx) + keys, err := sc.getKeys(ctx) if err != nil { - if !errors.Is(err, errStatsDisabled) { - klog.InfoS("Error getting keys", "err", err) - } + klog.InfoS("Error getting keys", "err", err) return } sc.keysLock.Lock() @@ -149,7 +124,7 @@ func (sc *statsCache) cleanKeysIfNeeded(ctx context.Context) { sc.cleanKeys(keys) } -func (sc *statsCache) cleanKeys(keepKeys []string) { +func (sc *resourceSizeEstimator) cleanKeys(keepKeys []string) { newKeys := make(map[string]sizeRevision, len(keepKeys)) for _, key := range keepKeys { // Handle cacher keys not having prefix. @@ -171,14 +146,14 @@ func (sc *statsCache) cleanKeys(keepKeys []string) { sc.lastKeyCleanup.Store(&now) } -func (sc *statsCache) keySizes() (totalSize int64) { +func (sc *resourceSizeEstimator) keySizes() (totalSize int64) { for _, sizeRevision := range sc.keys { totalSize += sizeRevision.sizeBytes } return totalSize } -func (sc *statsCache) Update(kvs []*mvccpb.KeyValue) { +func (sc *resourceSizeEstimator) Update(kvs []*mvccpb.KeyValue) { sc.keysLock.Lock() defer sc.keysLock.Unlock() for _, kv := range kvs { @@ -186,14 +161,14 @@ func (sc *statsCache) Update(kvs []*mvccpb.KeyValue) { } } -func (sc *statsCache) UpdateKey(kv *mvccpb.KeyValue) { +func (sc *resourceSizeEstimator) UpdateKey(kv *mvccpb.KeyValue) { sc.keysLock.Lock() defer sc.keysLock.Unlock() sc.updateKey(kv) } -func (sc *statsCache) updateKey(kv *mvccpb.KeyValue) { +func (sc *resourceSizeEstimator) updateKey(kv *mvccpb.KeyValue) { key := string(kv.Key) keySizeRevision := sc.keys[key] if keySizeRevision.revision >= kv.ModRevision { @@ -206,7 +181,7 @@ func (sc *statsCache) updateKey(kv *mvccpb.KeyValue) { } } -func (sc *statsCache) DeleteKey(kv *mvccpb.KeyValue) { +func (sc *resourceSizeEstimator) DeleteKey(kv *mvccpb.KeyValue) { sc.keysLock.Lock() defer sc.keysLock.Unlock() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats_test.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats_test.go index e67436f83..dabd141fa 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/stats_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/stats_test.go @@ -27,7 +27,7 @@ import ( func TestStatsCache(t *testing.T) { ctx := t.Context() - store := newStatsCache("/prefix", func(ctx context.Context) ([]string, error) { return []string{}, nil }) + store := newResourceSizeEstimator("/prefix", func(ctx context.Context) ([]string, error) { return []string{}, nil }) defer store.Close() stats, err := store.Stats(ctx) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store.go index d8b7a00a2..838483233 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store.go @@ -25,6 +25,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "go.etcd.io/etcd/api/v3/mvccpb" @@ -90,8 +91,10 @@ type store struct { resourcePrefix string newListFunc func() runtime.Object - stats *statsCache compactor Compactor + + collectorMux sync.RWMutex + resourceSizeEstimator *resourceSizeEstimator } var _ storage.Interface = (*store)(nil) @@ -142,7 +145,7 @@ func (a *abortOnFirstError) Aggregate(key string, err error) bool { func (a *abortOnFirstError) Err() error { return a.err } // New returns an etcd3 implementation of storage.Interface. -func New(c *kubernetes.Client, compactor Compactor, codec runtime.Codec, newFunc, newListFunc func() runtime.Object, prefix, resourcePrefix string, groupResource schema.GroupResource, transformer value.Transformer, leaseManagerConfig LeaseManagerConfig, decoder Decoder, versioner storage.Versioner) *store { +func New(c *kubernetes.Client, compactor Compactor, codec runtime.Codec, newFunc, newListFunc func() runtime.Object, prefix, resourcePrefix string, groupResource schema.GroupResource, transformer value.Transformer, leaseManagerConfig LeaseManagerConfig, decoder Decoder, versioner storage.Versioner) (*store, error) { // for compatibility with etcd2 impl. // no-op for default prefix of '/registry'. // keeps compatibility with etcd2 impl for custom prefixes that don't start with '/' @@ -151,6 +154,15 @@ func New(c *kubernetes.Client, compactor Compactor, codec runtime.Codec, newFunc // Ensure the pathPrefix ends in "/" here to simplify key concatenation later. pathPrefix += "/" } + if resourcePrefix == "" { + return nil, fmt.Errorf("resourcePrefix cannot be empty") + } + if resourcePrefix == "/" { + return nil, fmt.Errorf("resourcePrefix cannot be /") + } + if !strings.HasPrefix(resourcePrefix, "/") { + return nil, fmt.Errorf("resourcePrefix needs to start from /") + } listErrAggrFactory := defaultListErrorAggregatorFactory if utilfeature.DefaultFeatureGate.Enabled(features.AllowUnsafeMalformedObjectDeletion) { @@ -186,20 +198,15 @@ func New(c *kubernetes.Client, compactor Compactor, codec runtime.Codec, newFunc newListFunc: newListFunc, compactor: compactor, } - // Collecting stats requires properly set resourcePrefix to call getKeys. - if resourcePrefix != "" && utilfeature.DefaultFeatureGate.Enabled(features.SizeBasedListCostEstimate) { - stats := newStatsCache(pathPrefix, nil) - s.stats = stats - w.stats = stats - } + w.getResourceSizeEstimator = s.getResourceSizeEstimator w.getCurrentStorageRV = func(ctx context.Context) (uint64, error) { return s.GetCurrentResourceVersion(ctx) } if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) || utilfeature.DefaultFeatureGate.Enabled(features.WatchList) { etcdfeature.DefaultFeatureSupportChecker.CheckClient(c.Ctx(), c, storage.RequestWatchProgress) } - return s + return s, nil } func (s *store) CompactRevision() int64 { @@ -215,14 +222,21 @@ func (s *store) Versioner() storage.Versioner { } func (s *store) Close() { - if s.stats != nil { - s.stats.Close() + stats := s.getResourceSizeEstimator() + if stats != nil { + stats.Close() } } +func (s *store) getResourceSizeEstimator() *resourceSizeEstimator { + s.collectorMux.RLock() + defer s.collectorMux.RUnlock() + return s.resourceSizeEstimator +} + // Get implements storage.Interface.Get. func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, out runtime.Object) error { - preparedKey, err := s.prepareKey(key) + preparedKey, err := s.prepareKey(key, false) if err != nil { return err } @@ -258,7 +272,7 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ou // Create implements storage.Interface.Create. func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error { - preparedKey, err := s.prepareKey(key) + preparedKey, err := s.prepareKey(key, false) if err != nil { return err } @@ -328,7 +342,7 @@ func (s *store) Create(ctx context.Context, key string, obj, out runtime.Object, func (s *store) Delete( ctx context.Context, key string, out runtime.Object, preconditions *storage.Preconditions, validateDeletion storage.ValidateObjectFunc, cachedExistingObject runtime.Object, opts storage.DeleteOptions) error { - preparedKey, err := s.prepareKey(key) + preparedKey, err := s.prepareKey(key, false) if err != nil { return err } @@ -449,7 +463,7 @@ func (s *store) conditionalDelete( func (s *store) GuaranteedUpdate( ctx context.Context, key string, destination runtime.Object, ignoreNotFound bool, preconditions *storage.Preconditions, tryUpdate storage.UpdateFunc, cachedExistingObject runtime.Object) error { - preparedKey, err := s.prepareKey(key) + preparedKey, err := s.prepareKey(key, false) if err != nil { return err } @@ -630,21 +644,17 @@ func getNewItemFunc(listObj runtime.Object, v reflect.Value) func() runtime.Obje } } -func (s *store) Stats(ctx context.Context) (stats storage.Stats, err error) { - if s.stats != nil { - stats, err := s.stats.Stats(ctx) - if !errors.Is(err, errStatsDisabled) { - return stats, err - } +func (s *store) Stats(ctx context.Context) (storage.Stats, error) { + if collector := s.getResourceSizeEstimator(); collector != nil { + return collector.Stats(ctx) } + // returning stats without resource size + startTime := time.Now() - prefix, err := s.prepareKey(s.resourcePrefix) + prefix, err := s.prepareKey(s.resourcePrefix, true) if err != nil { return storage.Stats{}, err } - if !strings.HasSuffix(prefix, "/") { - prefix += "/" - } count, err := s.client.Kubernetes.Count(ctx, prefix, kubernetes.CountOptions{}) metrics.RecordEtcdRequest("listWithCount", s.groupResource, err, startTime) if err != nil { @@ -655,21 +665,25 @@ func (s *store) Stats(ctx context.Context) (stats storage.Stats, err error) { }, nil } -func (s *store) SetKeysFunc(keys storage.KeysFunc) { - if s.stats != nil { - s.stats.SetKeysFunc(keys) +func (s *store) EnableResourceSizeEstimation(getKeys storage.KeysFunc) error { + if getKeys == nil { + return errors.New("KeysFunc cannot be nil") } + s.collectorMux.Lock() + defer s.collectorMux.Unlock() + if s.resourceSizeEstimator != nil { + return errors.New("resourceSizeEstimator already enabled") + } + s.resourceSizeEstimator = newResourceSizeEstimator(s.pathPrefix, getKeys) + return nil } func (s *store) getKeys(ctx context.Context) ([]string, error) { startTime := time.Now() - prefix, err := s.prepareKey(s.resourcePrefix) + prefix, err := s.prepareKey(s.resourcePrefix, true) if err != nil { return nil, err } - if !strings.HasSuffix(prefix, "/") { - prefix += "/" - } resp, err := s.client.KV.Get(ctx, prefix, clientv3.WithPrefix(), clientv3.WithKeysOnly()) metrics.RecordEtcdRequest("listOnlyKeys", s.groupResource, err, startTime) if err != nil { @@ -720,7 +734,7 @@ func (s *store) GetCurrentResourceVersion(ctx context.Context) (uint64, error) { // GetList implements storage.Interface. func (s *store) GetList(ctx context.Context, key string, opts storage.ListOptions, listObj runtime.Object) error { - keyPrefix, err := s.prepareKey(key) + keyPrefix, err := s.prepareKey(key, opts.Recursive) if err != nil { return err } @@ -741,14 +755,6 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption return fmt.Errorf("need ptr to slice: %v", err) } - // For recursive lists, we need to make sure the key ended with "/" so that we only - // get children "directories". e.g. if we have key "/a", "/a/b", "/ab", getting keys - // with prefix "/a" will return all three, while with prefix "/a/" will return only - // "/a/b" which is the correct answer. - if opts.Recursive && !strings.HasSuffix(keyPrefix, "/") { - keyPrefix += "/" - } - // set the appropriate clientv3 options to filter the returned data set limit := opts.Predicate.Limit paging := opts.Predicate.Limit > 0 @@ -772,20 +778,13 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption metrics.RecordStorageListMetrics(s.groupResource, numFetched, numEvald, numReturn) }() - metricsOp := "get" - if opts.Recursive { - metricsOp = "list" - } - aggregator := s.listErrAggrFactory() for { - startTime := time.Now() getResp, err = s.getList(ctx, keyPrefix, opts.Recursive, kubernetes.ListOptions{ Revision: withRev, Limit: limit, Continue: continueKey, }) - metrics.RecordEtcdRequest(metricsOp, s.groupResource, err, startTime) if err != nil { if errors.Is(err, etcdrpc.ErrFutureRev) { currentRV, getRVErr := s.GetCurrentResourceVersion(ctx) @@ -818,9 +817,6 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption } else { growSlice(v, 2048, len(getResp.Kvs)) } - if s.stats != nil { - s.stats.Update(getResp.Kvs) - } // take items from the response until the bucket is full, filtering as we go for i, kv := range getResp.Kvs { @@ -899,25 +895,40 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption if err != nil { return err } - return s.versioner.UpdateList(listObj, uint64(withRev), continueValue, remainingItemCount) + if err := s.versioner.UpdateList(listObj, uint64(withRev), continueValue, remainingItemCount); err != nil { + return err + } + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + opts.Predicate.SetShardInfoOnList(listObj) + } + return nil } -func (s *store) getList(ctx context.Context, keyPrefix string, recursive bool, options kubernetes.ListOptions) (kubernetes.ListResponse, error) { +func (s *store) getList(ctx context.Context, keyPrefix string, recursive bool, options kubernetes.ListOptions) (resp kubernetes.ListResponse, err error) { + startTime := time.Now() if recursive { - return s.client.Kubernetes.List(ctx, keyPrefix, options) - } - getResp, err := s.client.Kubernetes.Get(ctx, keyPrefix, kubernetes.GetOptions{ - Revision: options.Revision, - }) - var resp kubernetes.ListResponse - if getResp.KV != nil { - resp.Kvs = []*mvccpb.KeyValue{getResp.KV} - resp.Count = 1 - resp.Revision = getResp.Revision + resp, err = s.client.Kubernetes.List(ctx, keyPrefix, options) + metrics.RecordEtcdRequest("list", s.groupResource, err, startTime) } else { - resp.Kvs = []*mvccpb.KeyValue{} - resp.Count = 0 - resp.Revision = getResp.Revision + var getResp kubernetes.GetResponse + getResp, err = s.client.Kubernetes.Get(ctx, keyPrefix, kubernetes.GetOptions{ + Revision: options.Revision, + }) + metrics.RecordEtcdRequest("get", s.groupResource, err, startTime) + if getResp.KV != nil { + resp.Kvs = []*mvccpb.KeyValue{getResp.KV} + resp.Count = 1 + resp.Revision = getResp.Revision + } else { + resp.Kvs = []*mvccpb.KeyValue{} + resp.Count = 0 + resp.Revision = getResp.Revision + } + } + + stats := s.getResourceSizeEstimator() + if len(resp.Kvs) > 0 && stats != nil { + stats.Update(resp.Kvs) } return resp, err } @@ -955,7 +966,7 @@ func growSlice(v reflect.Value, maxCapacity int, sizes ...int) { // Watch implements storage.Interface.Watch. func (s *store) Watch(ctx context.Context, key string, opts storage.ListOptions) (watch.Interface, error) { - preparedKey, err := s.prepareKey(key) + preparedKey, err := s.prepareKey(key, opts.Recursive) if err != nil { return nil, err } @@ -1102,21 +1113,10 @@ func (s *store) validateMinimumResourceVersion(minimumResourceVersion string, ac return nil } -func (s *store) prepareKey(key string) (string, error) { - if key == ".." || - strings.HasPrefix(key, "../") || - strings.HasSuffix(key, "/..") || - strings.Contains(key, "/../") { - return "", fmt.Errorf("invalid key: %q", key) - } - if key == "." || - strings.HasPrefix(key, "./") || - strings.HasSuffix(key, "/.") || - strings.Contains(key, "/./") { - return "", fmt.Errorf("invalid key: %q", key) - } - if key == "" || key == "/" { - return "", fmt.Errorf("empty key: %q", key) +func (s *store) prepareKey(key string, recursive bool) (string, error) { + key, err := storage.PrepareKey(s.resourcePrefix, key, recursive) + if err != nil { + return "", err } // We ensured that pathPrefix ends in '/' in construction, so skip any leading '/' in the key now. startIndex := 0 diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store_test.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store_test.go index 3c01fd158..04e9f64df 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/store_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/store_test.go @@ -177,7 +177,7 @@ func TestListPaging(t *testing.T) { func TestGetListNonRecursive(t *testing.T) { ctx, store, client := testSetup(t) - storagetesting.RunTestGetListNonRecursive(ctx, t, increaseRV(client.Client), store) + storagetesting.RunTestGetListNonRecursive(ctx, t, increaseRVFunc(client.Client), store) } func TestGetListRecursivePrefix(t *testing.T) { @@ -185,6 +185,11 @@ func TestGetListRecursivePrefix(t *testing.T) { storagetesting.RunTestGetListRecursivePrefix(ctx, t, store) } +func TestKeySchema(t *testing.T) { + ctx, store, _ := testSetup(t) + storagetesting.RunTestKeySchema(ctx, t, store) +} + type storeWithPrefixTransformer struct { *store } @@ -259,14 +264,14 @@ func TestList(t *testing.T) { func TestConsistentList(t *testing.T) { ctx, store, client := testSetup(t) - storagetesting.RunTestConsistentList(ctx, t, store, increaseRV(client.Client), false, true, false) + storagetesting.RunTestConsistentList(ctx, t, store, increaseRVFunc(client.Client), false, true, false) } func TestCompactRevision(t *testing.T) { // Test requires store to observe extenal changes to compaction revision, requiring dedicated watch on compact key which is enabled by ListFromCacheSnapshot. featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ListFromCacheSnapshot, true) ctx, store, client := testSetup(t) - storagetesting.RunTestCompactRevision(ctx, t, store, increaseRV(client.Client), compactStorage(store, client.Client)) + storagetesting.RunTestCompactRevision(ctx, t, store, increaseRVFunc(client.Client), compactStorage(store, client.Client)) } func checkStorageCallsInvariants(transformer *storagetesting.PrefixTransformer, recorder *storagetesting.KVRecorder) storagetesting.CallsValidation { @@ -360,11 +365,13 @@ func compactStorage(s *store, client *clientv3.Client) storagetesting.Compaction } } -func increaseRV(client *clientv3.Client) storagetesting.IncreaseRVFunc { - return func(ctx context.Context, t *testing.T) { - if _, err := client.KV.Put(ctx, "increaseRV", "ok"); err != nil { +func increaseRVFunc(client *clientv3.Client) storagetesting.IncreaseRVFunc { + return func(ctx context.Context, t *testing.T) int64 { + resp, err := client.KV.Put(ctx, "increaseRV", "ok") + if err != nil { t.Fatalf("Could not update increaseRV: %v", err) } + return resp.Header.Revision } } @@ -381,10 +388,13 @@ func TestListResourceVersionMatch(t *testing.T) { func TestStats(t *testing.T) { for _, sizeBasedListCostEstimate := range []bool{true, false} { t.Run(fmt.Sprintf("SizeBasedListCostEstimate=%v", sizeBasedListCostEstimate), func(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SizeBasedListCostEstimate, sizeBasedListCostEstimate) - // Match transformer with cacher tests. ctx, store, _ := testSetup(t) - store.SetKeysFunc(store.getKeys) + if sizeBasedListCostEstimate { + err := store.EnableResourceSizeEstimation(store.getKeys) + if err != nil { + t.Fatal(err) + } + } storagetesting.RunTestStats(ctx, t, store, store.codec, store.transformer, sizeBasedListCostEstimate) }) } @@ -519,15 +529,15 @@ func TestLeaseMaxObjectCount(t *testing.T) { expectAttachedCount int64 }{ { - key: "testkey1", + key: "/pods/testkey1", expectAttachedCount: 1, }, { - key: "testkey2", + key: "/pods/testkey2", expectAttachedCount: 2, }, { - key: "testkey3", + key: "/pods/testkey3", // We assume each time has 1 object attached to the lease // so after granting a new lease, the recorded count is set to 1 expectAttachedCount: 1, @@ -610,7 +620,7 @@ func withDefaults(options *setupOptions) { options.newFunc = newPod options.newListFunc = newPodList options.prefix = "" - options.resourcePrefix = "/pods" + options.resourcePrefix = "/pods/" options.groupResource = schema.GroupResource{Resource: "pods"} options.transformer = newTestTransformer() options.leaseConfig = newTestLeaseManagerConfig() @@ -628,7 +638,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, *store, *kub versioner := storage.APIObjectVersioner{} compactor := NewCompactor(client.Client, 0, clock.RealClock{}, nil) t.Cleanup(compactor.Stop) - store := New( + store, err := New( client, compactor, setupOpts.codec, @@ -642,23 +652,40 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, *store, *kub NewDefaultDecoder(setupOpts.codec, versioner), versioner, ) + if err != nil { + t.Fatal(err) + } t.Cleanup(store.Close) ctx := context.Background() return ctx, store, client } -func TestValidateKey(t *testing.T) { +func TestPrepareKey(t *testing.T) { validKeys := []string{ - "/foo/bar/baz/a.b.c/", - "/foo", - "foo/bar/baz", - "/foo/bar..baz/", - "/foo/bar..", - "foo", - "foo/bar", - "/foo/bar/", + "/pods/foo/bar/baz/a.b.c/", + "/pods/foo", + "/pods/foo/bar/baz", + "/pods/foo/bar..baz/", + "/pods/foo/bar..", + "/pods/foo", + "/pods/foo/bar", + "/pods/foo/bar/", + "/pods/", } invalidKeys := []string{ + "/pods", + "/pods/foo/bar/../a.b.c/", + "/pods/..", + "/pods/../", + "/pods/foo/bar/..", + "/pods/../foo/bar", + "/pods/../foo", + "/pods/foo/bar/../", + "/pods/.", + "/pods/./", + "/pods/foo/.", + "/pods/./bar", + "/pods/foo/./bar/", "/foo/bar/../a.b.c/", "..", "/..", @@ -681,21 +708,46 @@ func TestValidateKey(t *testing.T) { ) _, store, _ := testSetup(t, withPrefix(pathPrefix)) - for _, key := range validKeys { - k, err := store.prepareKey(key) - if err != nil { - t.Errorf("key %q should be valid; unexpected error: %v", key, err) - } else if !strings.HasPrefix(k, expectPrefix) { - t.Errorf("key %q should have prefix %q", k, expectPrefix) + t.Run("Non-recursive", func(t *testing.T) { + for _, key := range validKeys { + preparedKey, err := store.prepareKey(key, false) + if err != nil { + t.Errorf("preparing key %q should be valid; unexpected error: %v", key, err) + continue + } + if !strings.HasPrefix(preparedKey, expectPrefix) { + t.Errorf("prepared key %q should have prefix %q", preparedKey, expectPrefix) + } + if !strings.HasSuffix(preparedKey, key) { + t.Errorf("Prepared non-recursive key %q should have suffix %q", preparedKey, key) + } } - } + }) - for _, key := range invalidKeys { - _, err := store.prepareKey(key) - if err == nil { - t.Errorf("key %q should be invalid", key) + t.Run("Recursive", func(t *testing.T) { + for _, key := range validKeys { + preparedKey, err := store.prepareKey(key, true) + if err != nil { + t.Errorf("preparing key %q should be valid; unexpected error: %v", key, err) + continue + } + if !strings.HasPrefix(preparedKey, expectPrefix) { + t.Errorf("prepared key %q should have prefix %q", preparedKey, expectPrefix) + } + if !strings.HasSuffix(preparedKey, "/") { + t.Errorf("Prepared non-recursive key %q should have suffix '/'", preparedKey) + } } - } + }) + + t.Run("Invalid", func(t *testing.T) { + for _, key := range invalidKeys { + _, err := store.prepareKey(key, false) + if err == nil { + t.Errorf("key %q should be invalid", key) + } + } + }) } func TestInvalidKeys(t *testing.T) { @@ -917,14 +969,14 @@ func TestGetCurrentResourceVersion(t *testing.T) { } } createPod := func(obj *example.Pod) *example.Pod { - key := "pods/" + obj.Namespace + "/" + obj.Name + key := "/pods/" + obj.Namespace + "/" + obj.Name out := &example.Pod{} err := store.Create(context.TODO(), key, obj, out, 0) require.NoError(t, err) return out } getPod := func(name, ns string) *example.Pod { - key := "pods/" + ns + "/" + name + key := "/pods/" + ns + "/" + name out := &example.Pod{} err := store.Get(context.TODO(), key, storage.GetOptions{}, out) require.NoError(t, err) @@ -988,8 +1040,8 @@ func BenchmarkStatsCacheCleanKeys(b *testing.B) { if err != nil { b.Fatal(err) } - if len(store.stats.keys) < namespaceCount*podPerNamespaceCount { - b.Fatalf("Unexpected number of keys in stats, want: %d, got: %d", namespaceCount*podPerNamespaceCount, len(store.stats.keys)) + if len(store.resourceSizeEstimator.keys) < namespaceCount*podPerNamespaceCount { + b.Fatalf("Unexpected number of keys in stats, want: %d, got: %d", namespaceCount*podPerNamespaceCount, len(store.resourceSizeEstimator.keys)) } // Get keys to measure only cleanupKeys time keys, err := store.getKeys(ctx) @@ -998,15 +1050,15 @@ func BenchmarkStatsCacheCleanKeys(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - store.stats.cleanKeys(keys) + store.resourceSizeEstimator.cleanKeys(keys) } - if len(store.stats.keys) < namespaceCount*podPerNamespaceCount { - b.Fatalf("Unexpected number of keys in stats, want: %d, got: %d", namespaceCount*podPerNamespaceCount, len(store.stats.keys)) + if len(store.resourceSizeEstimator.keys) < namespaceCount*podPerNamespaceCount { + b.Fatalf("Unexpected number of keys in stats, want: %d, got: %d", namespaceCount*podPerNamespaceCount, len(store.resourceSizeEstimator.keys)) } } func TestPrefixGetKeys(t *testing.T) { - ctx, store, c := testSetup(t, withPrefix("/registry"), withResourcePrefix("pods")) + ctx, store, c := testSetup(t, withPrefix("/registry"), withResourcePrefix("/pods")) _, err := c.KV.Put(ctx, "key", "a") if err != nil { t.Fatal(err) @@ -1042,40 +1094,27 @@ func TestPrefixStats(t *testing.T) { tcs := []struct { name string estimate bool - setKeys bool expectStats storage.Stats }{ { - name: "SizeBasedListCostEstimate=false,SetKeys=false", - setKeys: false, + name: "Estimate=false", estimate: false, expectStats: storage.Stats{ObjectCount: 1}, }, { - name: "SizeBasedListCostEstimate=false,SetKeys=true", - setKeys: true, - estimate: false, - expectStats: storage.Stats{ObjectCount: 1}, - }, - { - name: "SizeBasedListCostEstimate=true,SetKeys=false", - setKeys: false, - estimate: true, - expectStats: storage.Stats{ObjectCount: 1}, - }, - { - name: "SizeBasedListCostEstimate=true,SetKeys=true", - setKeys: true, + name: "Estimate=true", estimate: true, expectStats: storage.Stats{ObjectCount: 1, EstimatedAverageObjectSizeBytes: 3}, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SizeBasedListCostEstimate, tc.estimate) - ctx, store, c := testSetup(t, withPrefix("/registry"), withResourcePrefix("pods")) - if tc.setKeys { - store.SetKeysFunc(store.getKeys) + ctx, store, c := testSetup(t, withPrefix("/registry"), withResourcePrefix("/pods/")) + if tc.estimate { + err := store.EnableResourceSizeEstimation(store.getKeys) + if err != nil { + t.Fatal(err) + } } _, err := c.KV.Put(ctx, "key", "a") if err != nil { @@ -1099,7 +1138,7 @@ func TestPrefixStats(t *testing.T) { listOut := &example.PodList{} // Ignore error as decode is expected to fail - _ = store.GetList(ctx, "pods", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, listOut) + _ = store.GetList(ctx, "/pods/", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, listOut) gotStats, err := store.Stats(ctx) if err != nil { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/test_server.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/test_server.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/test_server.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/test_server.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/testingcert/certificates.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/testingcert/certificates.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/testingcert/certificates.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/testingcert/certificates.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/utils.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/utils.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testing/utils.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testing/utils.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testserver/test_server.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testserver/test_server.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/testserver/test_server.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/testserver/test_server.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher.go index 76f65f160..0559383f8 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" clientv3 "go.etcd.io/etcd/client/v3" @@ -55,44 +56,44 @@ const ( var defaultWatcherMaxLimit int64 = maxLimit // fatalOnDecodeError is used during testing to panic the server if watcher encounters a decoding error -var fatalOnDecodeError = false +var fatalOnDecodeError atomic.Bool func init() { // check to see if we are running in a test environment - TestOnlySetFatalOnDecodeError(true) - fatalOnDecodeError, _ = strconv.ParseBool(os.Getenv("KUBE_PANIC_WATCH_DECODE_ERROR")) + b, _ := strconv.ParseBool(os.Getenv("KUBE_PANIC_WATCH_DECODE_ERROR")) + TestOnlySetFatalOnDecodeError(b) } // TestOnlySetFatalOnDecodeError should only be used for cases where decode errors are expected and need to be tested. e.g. conversion webhooks. func TestOnlySetFatalOnDecodeError(b bool) { - fatalOnDecodeError = b + fatalOnDecodeError.Store(b) } type watcher struct { - client *clientv3.Client - codec runtime.Codec - newFunc func() runtime.Object - objectType string - groupResource schema.GroupResource - versioner storage.Versioner - transformer value.Transformer - getCurrentStorageRV func(context.Context) (uint64, error) - stats *statsCache + client *clientv3.Client + codec runtime.Codec + newFunc func() runtime.Object + objectType string + groupResource schema.GroupResource + versioner storage.Versioner + transformer value.Transformer + getCurrentStorageRV func(context.Context) (uint64, error) + getResourceSizeEstimator func() *resourceSizeEstimator } // watchChan implements watch.Interface. type watchChan struct { - watcher *watcher - key string - initialRev int64 - recursive bool - progressNotify bool - internalPred storage.SelectionPredicate - ctx context.Context - cancel context.CancelFunc - incomingEventChan chan *event - resultChan chan watch.Event - stats *statsCache + watcher *watcher + key string + initialRev int64 + recursive bool + progressNotify bool + internalPred storage.SelectionPredicate + ctx context.Context + cancel context.CancelFunc + incomingEventChan chan *event + resultChan chan watch.Event + getResourceSizeEstimator func() *resourceSizeEstimator } // Watch watches on a key and returns a watch.Interface that transfers relevant notifications. @@ -104,7 +105,7 @@ type watchChan struct { // pred must be non-nil. Only if opts.Predicate matches the change, it will be returned. func (w *watcher) Watch(ctx context.Context, key string, rev int64, opts storage.ListOptions) (watch.Interface, error) { if opts.Recursive && !strings.HasSuffix(key, "/") { - key += "/" + return nil, fmt.Errorf(`recursive key needs to end with "/"`) } if opts.ProgressNotify && w.newFunc == nil { return nil, apierrors.NewInternalError(errors.New("progressNotify for watch is unsupported by the etcd storage because no newFunc was provided")) @@ -128,15 +129,15 @@ func (w *watcher) Watch(ctx context.Context, key string, rev int64, opts storage func (w *watcher) createWatchChan(ctx context.Context, key string, rev int64, recursive, progressNotify bool, pred storage.SelectionPredicate) *watchChan { wc := &watchChan{ - watcher: w, - key: key, - initialRev: rev, - recursive: recursive, - progressNotify: progressNotify, - internalPred: pred, - incomingEventChan: make(chan *event, incomingBufSize), - resultChan: make(chan watch.Event, outgoingBufSize), - stats: w.stats, + watcher: w, + key: key, + initialRev: rev, + recursive: recursive, + progressNotify: progressNotify, + internalPred: pred, + incomingEventChan: make(chan *event, incomingBufSize), + resultChan: make(chan watch.Event, outgoingBufSize), + getResourceSizeEstimator: w.getResourceSizeEstimator, } if pred.Empty() { // The filter doesn't filter out any object. @@ -385,6 +386,7 @@ func (wc *watchChan) startWatching(watchClosedCh chan struct{}, initialEventsEnd opts = append(opts, clientv3.WithProgressNotify()) } wch := wc.watcher.client.Watch(wc.ctx, wc.key, opts...) + estimator := wc.getResourceSizeEstimator() for wres := range wch { if wres.Err() != nil { err := wres.Err() @@ -405,12 +407,12 @@ func (wc *watchChan) startWatching(watchClosedCh chan struct{}, initialEventsEnd } for _, e := range wres.Events { - if wc.stats != nil { + if estimator != nil { switch e.Type { case clientv3.EventTypePut: - wc.stats.UpdateKey(e.Kv) + estimator.UpdateKey(e.Kv) case clientv3.EventTypeDelete: - wc.stats.DeleteKey(e.Kv) + estimator.DeleteKey(e.Kv) } } metrics.RecordEtcdEvent(wc.watcher.groupResource) @@ -757,7 +759,7 @@ func (w *watcher) transformIfCorruptObjectError(e *event, err error) error { func decodeObj(codec runtime.Codec, versioner storage.Versioner, data []byte, rev int64) (_ runtime.Object, err error) { obj, err := runtime.Decode(codec, []byte(data)) if err != nil { - if fatalOnDecodeError { + if fatalOnDecodeError.Load() { // we are running in a test environment and thus an // error here is due to a coder mistake if the defer // does not catch it diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher_test.go index 88fcd656c..832186721 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/etcd3/watcher_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/etcd3/watcher_test.go @@ -108,7 +108,7 @@ func TestProgressNotify(t *testing.T) { clusterConfig.WatchProgressNotifyInterval = time.Second ctx, store, client := testSetup(t, withClientConfig(clusterConfig)) - storagetesting.RunOptionalTestProgressNotify(ctx, t, store, increaseRV(client.Client)) + storagetesting.RunOptionalTestProgressNotify(ctx, t, store, increaseRVFunc(client.Client)) } func TestWatchWithUnsafeDelete(t *testing.T) { @@ -227,7 +227,7 @@ func TestTooLargeResourceVersionErrorForWatchList(t *testing.T) { t.Fatalf("Unable to convert NewTooLargeResourceVersionError to apierrors.StatusError") } - w, err := store.watcher.Watch(ctx, "/abc", int64(102), requestOpts) + w, err := store.watcher.Watch(ctx, "/abc/", int64(102), requestOpts) if err != nil { t.Fatal(err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/feature/feature_support_checker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/feature/feature_support_checker.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/feature/feature_support_checker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/feature/feature_support_checker.go index 77f5ab05c..e664d22a6 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/feature/feature_support_checker.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/feature/feature_support_checker.go @@ -109,7 +109,7 @@ func (f *defaultFeatureSupportChecker) checkClient(ctx context.Context, c client } f.checkingEndpoint[ep] = struct{}{} go func(ep string) { - defer runtime.HandleCrash() + defer runtime.HandleCrashWithContext(ctx) err := delayFunc.Until(ctx, true, true, func(ctx context.Context) (done bool, err error) { internalErr := f.clientSupportsRequestWatchProgress(ctx, c, ep) return internalErr == nil, nil diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/feature/feature_support_checker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/feature/feature_support_checker_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/feature/feature_support_checker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/feature/feature_support_checker_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces.go index a07dda562..be9980cfc 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces.go @@ -19,6 +19,7 @@ package storage import ( "context" "fmt" + "strings" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -268,9 +269,8 @@ type Interface interface { // This method issues an empty list request and reads only the ResourceVersion from the object metadata GetCurrentResourceVersion(ctx context.Context) (uint64, error) - // SetKeysFunc allows to override the function used to get keys from storage. - // This allows to replace default function that fetches keys from storage with one using cache. - SetKeysFunc(KeysFunc) + // EnableResourceSizeEstimation enables estimating resource size by providing function get keys from storage. + EnableResourceSizeEstimation(KeysFunc) error // CompactRevision returns latest observed revision that was compacted. // Without ListFromCacheSnapshot enabled only locally executed compaction will be observed. @@ -391,3 +391,32 @@ type Stats struct { // Value is an estimate, meaning it doesn't need to provide accurate nor consistent. EstimatedAverageObjectSizeBytes int64 } + +func PrepareKey(resourcePrefix, key string, recursive bool) (string, error) { + if key == ".." || + strings.HasPrefix(key, "../") || + strings.HasSuffix(key, "/..") || + strings.Contains(key, "/../") { + return "", fmt.Errorf("invalid key: %q", key) + } + if key == "." || + strings.HasPrefix(key, "./") || + strings.HasSuffix(key, "/.") || + strings.Contains(key, "/./") { + return "", fmt.Errorf("invalid key: %q", key) + } + if key == "" || key == "/" { + return "", fmt.Errorf("empty key: %q", key) + } + // For recursive requests, we need to make sure the key ended with "/" so that we only + // get children "directories". e.g. if we have key "/a", "/a/b", "/ab", getting keys + // with prefix "/a" will return all three, while with prefix "/a/" will return only + // "/a/b" which is the correct answer. + if recursive && !strings.HasSuffix(key, "/") { + key += "/" + } + if !strings.HasPrefix(key, resourcePrefix) { + return "", fmt.Errorf("invalid key: %q lacks resource prefix: %q", key, resourcePrefix) + } + return key, nil +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces_test.go index 8f4826694..3826c1cea 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/interfaces_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/interfaces_test.go @@ -173,7 +173,6 @@ func TestValidateListOptions(t *testing.T) { }, } for _, tt := range testCases { - tt := tt t.Run(tt.name, func(t *testing.T) { withRev, continueKey, err := ValidateListOptions("", APIObjectVersioner{}, tt.opts) if len(tt.expectedError) > 0 { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/names/generate.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/names/generate.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/names/generate.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/names/generate.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/names/generate_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/names/generate_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/names/generate_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/names/generate_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/selection_predicate.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/selection_predicate.go similarity index 80% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/selection_predicate.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/selection_predicate.go index 480b5a893..37d470b25 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/selection_predicate.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/selection_predicate.go @@ -20,10 +20,14 @@ import ( "context" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/sharding" "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) // AttrFunc returns label and field sets and the uninitialized flag for List or Watch to match. @@ -82,12 +86,19 @@ type SelectionPredicate struct { Limit int64 Continue string AllowWatchBookmarks bool + // ShardSelector is the parsed shard selector for filtering objects by hash range. + ShardSelector sharding.Selector } // Matches returns true if the given object's labels and fields (as // returned by s.GetAttrs) match s.Label and s.Field. An error is // returned if s.GetAttrs fails. func (s *SelectionPredicate) Matches(obj runtime.Object) (bool, error) { + if utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + if matched, err := s.MatchesSharding(obj); err != nil || !matched { + return matched, err + } + } if s.Empty() { return true, nil } @@ -168,6 +179,30 @@ func (s *SelectionPredicate) MatcherIndex(ctx context.Context) []MatchValue { return result } +// MatchesSharding returns true if the given object matches the sharding configuration. +// If ShardSelector is set and non-empty, it delegates to ShardSelector.Matches(). +func (s *SelectionPredicate) MatchesSharding(obj runtime.Object) (bool, error) { + if !utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + return true, nil + } + if s.ShardSelector != nil && !s.ShardSelector.Empty() { + return s.ShardSelector.Matches(obj) + } + return true, nil +} + +// SetShardInfoOnList sets shard metadata on the list response if sharding is active. +func (s *SelectionPredicate) SetShardInfoOnList(listObj runtime.Object) { + if !utilfeature.DefaultFeatureGate.Enabled(features.ShardedListAndWatch) { + return + } + if s.ShardSelector != nil && !s.ShardSelector.Empty() { + if setter, ok := listObj.(metav1.ShardedListInterface); ok { + setter.SetShardInfo(&metav1.ShardInfo{Selector: s.ShardSelector.String()}) + } + } +} + func isNamespaceScopedRequest(ctx context.Context) (string, bool) { re, _ := request.RequestInfoFrom(ctx) if re == nil || len(re.Namespace) == 0 { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/selection_predicate_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/selection_predicate_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/selection_predicate_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/selection_predicate_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/etcd3.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/etcd3.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/etcd3.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/etcd3.go index c8ee0a128..6b60601a7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/etcd3.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/etcd3.go @@ -29,7 +29,7 @@ import ( "sync" "time" - grpcprom "github.com/grpc-ecosystem/go-grpc-prometheus" + grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" "go.etcd.io/etcd/client/pkg/v3/logutil" "go.etcd.io/etcd/client/pkg/v3/transport" clientv3 "go.etcd.io/etcd/client/v3" @@ -79,12 +79,15 @@ const ( // https://github.com/kubernetes/kubernetes/issues/111476 for more. var etcd3ClientLogger *zap.Logger +// grpcpromClientMetrics is a singleton instance of grpc client prometheus metrics. +var grpcpromClientMetrics = grpcprom.NewClientMetrics() + func init() { - // grpcprom auto-registers (via an init function) their client metrics, since we are opting out of - // using the global prometheus registry and using our own wrapped global registry, - // we need to explicitly register these metrics to our global registry here. + // Since we are opting out of using the global prometheus registry and using + // our own wrapped global registry, we need to explicitly register the client + // metrics to our global registry here. // For reference: https://github.com/kubernetes/kubernetes/pull/81387 - legacyregistry.RawMustRegister(grpcprom.DefaultClientMetrics) + legacyregistry.RawMustRegister(grpcpromClientMetrics) dbMetricsMonitors = make(map[string]struct{}) l, err := logutil.CreateDefaultZapLogger(etcdClientDebugLevel()) @@ -313,8 +316,8 @@ var newETCD3Client = func(c storagebackend.TransportConfig) (*kubernetes.Client, // // these optional interceptors will be placed after the default ones. // which seems to be what we want as the metrics will be collected on each attempt (retry) - grpc.WithChainUnaryInterceptor(grpcprom.UnaryClientInterceptor), - grpc.WithChainStreamInterceptor(grpcprom.StreamClientInterceptor), + grpc.WithChainUnaryInterceptor(grpcpromClientMetrics.UnaryClientInterceptor()), + grpc.WithChainStreamInterceptor(grpcpromClientMetrics.StreamClientInterceptor()), } if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerTracing) { tracingOpts := []otelgrpc.Option{ @@ -440,6 +443,8 @@ func newETCD3Storage(c storagebackend.ConfigForResource, newFunc, newListFunc fu stopDBSizeMonitor, err := startDBSizeMonitorPerEndpoint(client.Client, c.DBMetricPollInterval) if err != nil { + stopCompactor() + _ = client.Close() return nil, nil, err } @@ -455,7 +460,13 @@ func newETCD3Storage(c storagebackend.ConfigForResource, newFunc, newListFunc fu transformer = etcd3.WithCorruptObjErrorHandlingTransformer(transformer) decoder = etcd3.WithCorruptObjErrorHandlingDecoder(decoder) } - store := etcd3.New(client, compactor, c.Codec, newFunc, newListFunc, c.Prefix, resourcePrefix, c.GroupResource, transformer, c.LeaseManagerConfig, decoder, versioner) + store, err := etcd3.New(client, compactor, c.Codec, newFunc, newListFunc, c.Prefix, resourcePrefix, c.GroupResource, transformer, c.LeaseManagerConfig, decoder, versioner) + if err != nil { + stopCompactor() + stopDBSizeMonitor() + _ = client.Close() + return nil, nil, err + } var once sync.Once destroyFunc := func() { // we know that storage destroy funcs are called multiple times (due to reuse in subresources). diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/etcd3_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/etcd3_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/etcd3_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/etcd3_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/factory.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/factory.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/factory.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/factory.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/factory_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/factory_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/factory_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/factory_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/tls_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/tls_test.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/tls_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/tls_test.go index 2a93747f4..a095edf65 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/storagebackend/factory/tls_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/storagebackend/factory/tls_test.go @@ -80,12 +80,12 @@ func TestTLSConnection(t *testing.T) { }, Codec: codec, } - storage, destroyFunc, err := newETCD3Storage(*cfg.ForResource(schema.GroupResource{Resource: "pods"}), nil, nil, "") + storage, destroyFunc, err := newETCD3Storage(*cfg.ForResource(schema.GroupResource{Resource: "pods"}), nil, nil, "/pods") defer destroyFunc() if err != nil { t.Fatal(err) } - err = storage.Create(context.TODO(), "/abc", &example.Pod{}, nil, 0) + err = storage.Create(context.TODO(), "/pods/abc", &example.Pod{}, nil, 0) if err != nil { t.Fatalf("Create failed: %v", err) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/recorder.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/recorder.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/recorder.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/recorder.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_benchmarks.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_benchmarks.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_benchmarks.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_benchmarks.go index 6bb4acdee..a919546fe 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_benchmarks.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_benchmarks.go @@ -57,7 +57,7 @@ func RunBenchmarkStoreListCreate(ctx context.Context, b *testing.B, store storag panic(fmt.Sprintf("Unexpected error %s", err)) } listOut := &example.PodList{} - err = store.GetList(ctx, "/pods", storage.ListOptions{ + err = store.GetList(ctx, "/pods/", storage.ListOptions{ Recursive: true, ResourceVersion: podOut.ResourceVersion, ResourceVersionMatch: match, diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_tests.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_tests.go similarity index 80% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_tests.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_tests.go index 196b637f9..669af09d7 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/store_tests.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/store_tests.go @@ -33,6 +33,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.etcd.io/etcd/client/v3/kubernetes" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -193,19 +194,18 @@ func RunTestGet(ctx context.Context, t *testing.T, store storage.Interface) { rv: strconv.FormatInt(math.MaxInt64, 10), }, { name: "get non-existing", - key: "/non-existing", + key: "/pods/non-existing", ignoreNotFound: false, expectNotFoundErr: true, }, { name: "get non-existing, ignore not found", - key: "/non-existing", + key: "/pods/non-existing", ignoreNotFound: true, expectNotFoundErr: false, expectedOut: &example.Pod{}, }} for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { // For some asynchronous implementations of storage interface (in particular watchcache), // certain requests may impact result of further requests. As an example, if we first @@ -258,7 +258,7 @@ func RunTestUnconditionalDelete(ctx context.Context, t *testing.T, store storage expectNotFoundErr: false, }, { name: "non-existing key", - key: "/non-existing", + key: "/pods/non-existing", expectedObj: nil, expectNotFoundErr: true, }} @@ -640,17 +640,15 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/second", storageOpts, list); err != nil { + if err := store.GetList(ctx, "/pods/second", storageOpts, list); err != nil { t.Errorf("Unexpected error: %v", err) } - continueRV, _ := strconv.Atoi(list.ResourceVersion) - secondContinuation, err := storage.EncodeContinue("/second/foo", "/second/", int64(continueRV)) + continueRV := mustParseResourceVersion(t, list.ResourceVersion) + secondContinuation, err := storage.EncodeContinue("/pods/second/foo", "/pods/second/", int64(continueRV)) if err != nil { t.Fatal(err) } - initialRV2, _ := strconv.Atoi(initialRV) - getAttrs := func(obj runtime.Object) (labels.Set, fields.Set, error) { pod := obj.(*example.Pod) return nil, fields.Set{"metadata.name": pod.Name, "spec.nodeName": pod.Spec.NodeName}, nil @@ -677,18 +675,18 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinueTooOld bool expectRV string expectRVFunc func(string) error - expectEtcdRequest func() []RecordedList + expectCacherRequestsToEtcd func() []RecordedList }{ { name: "rejects invalid resource version", - prefix: "/pods", + prefix: "/pods/", pred: storage.Everything, rv: "abc", expectError: true, }, { name: "rejects resource version and continue token", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Label: labels.Everything(), Field: fields.Everything(), @@ -700,7 +698,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "rejects resource version set too high", - prefix: "/pods", + prefix: "/pods/", pred: storage.Everything, rv: strconv.FormatInt(math.MaxInt64, 10), expectRVTooLarge: true, @@ -710,6 +708,16 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com prefix: "/pods/first/", pred: storage.Everything, expectedOut: []example.Pod{*updatedPod}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/first/", + }, + } + }, }, { name: "test List on existing key with resource version set to 0", @@ -726,6 +734,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[0].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[0].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/first/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[0].ResourceVersion)}, + }, + } + }, }, { name: "test List on existing key with resource version set before creation, match=Exact", @@ -735,6 +754,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[0].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[0].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[0].ResourceVersion)}, + }, + } + }, }, { name: "test List on existing key with resource version set after creation, match=Exact", @@ -744,6 +774,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[1].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[1].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[1].ResourceVersion)}, + }, + } + }, }, { name: "test List on existing key with resource version set before first write, match=Exact", @@ -752,6 +793,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: initialRV, rvMatch: metav1.ResourceVersionMatchExact, expectRVTooOld: true, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/first/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, initialRV)}, + }, + } + }, }, { name: "test List on existing key with resource version set to 0, match=NotOlderThan", @@ -800,6 +852,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: list.ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: list.ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/first/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV)}, + }, + } + }, }, { name: "test List on existing key with resource version set to current resource version, match=NotOlderThan", @@ -814,6 +877,16 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com prefix: "/pods/non-existing/", pred: storage.Everything, expectedOut: []example.Pod{}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/non-existing/", + }, + } + }, }, { name: "test List with pod name matching", @@ -823,6 +896,16 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Field: fields.ParseSelectorOrDie("metadata.name!=bar"), }, expectedOut: []example.Pod{}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/first/", + }, + } + }, }, { name: "test List with pod name matching with resource version set to current resource version, match=NotOlderThan", @@ -848,7 +931,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[1].Name+"\x00", int64(mustAtoi(currentRV))), expectedRemainingItemCount: ptr.To[int64](1), - expectEtcdRequest: func() []RecordedList { + expectCacherRequestsToEtcd: func() []RecordedList { if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { return nil } @@ -874,6 +957,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectedRemainingItemCount: ptr.To[int64](1), rv: list.ResourceVersion, expectRV: list.ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 1}, + }, + } + }, }, { name: "test List with limit at current resource version and match=Exact", @@ -890,6 +984,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: list.ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: list.ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 1}, + }, + } + }, }, { name: "test List with limit at current resource version and match=NotOlderThan", @@ -958,6 +1063,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[0].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[0].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[0].ResourceVersion), Limit: 1}, + }, + } + }, }, { name: "test List with limit at resource version after created and match=Exact", @@ -972,6 +1088,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[1].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[1].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[1].ResourceVersion), Limit: 1}, + }, + } + }, }, { name: "test List with pregenerated continue token", @@ -983,6 +1110,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Continue: secondContinuation, }, expectedOut: []example.Pod{*createdPods[2]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 1, Continue: "/registry/pods/second/foo"}, + }, + } + }, }, { name: "ignores resource version 0 for List with pregenerated continue token", @@ -995,12 +1133,33 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, rv: "0", expectedOut: []example.Pod{*createdPods[2]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 1, Continue: "/registry/pods/second/foo"}, + }, + } + }, }, { name: "test List with multiple levels of directories and expect flattened result", prefix: "/pods/second/", pred: storage.Everything, expectedOut: []example.Pod{*createdPods[1], *createdPods[2]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/second/", + }, + } + }, }, { name: "test List with multiple levels of directories and expect flattened result with current resource version and match=NotOlderThan", @@ -1012,7 +1171,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "test List with filter returning only one item, ensure only a single page returned", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1020,10 +1179,29 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[3]}, expectContinue: true, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Limit: 1}, + }, + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 2, Continue: "/registry/pods/first/bar\x00"}, + }, + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 4, Continue: "/registry/pods/second/foo\x00"}, + }, + } + }, }, { name: "test List with filter returning only one item, ensure only a single page returned with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1037,7 +1215,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "test List with filter returning only one item, covers the entire list", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1045,10 +1223,25 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[3]}, expectContinue: false, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 0, Limit: 2}, + }, + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 4, Continue: "/registry/pods/second/bar\x00"}, + }, + } + }, }, { name: "test List with filter returning only one item, covers the entire list with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1062,7 +1255,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "test List with filter returning only one item, covers the entire list, with resource version 0", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1074,7 +1267,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "test List with filter returning two items, more pages possible", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "bar"), Label: labels.Everything(), @@ -1082,10 +1275,21 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectContinue: true, expectedOut: []example.Pod{*updatedPod, *createdPods[1]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 0, Limit: 2}, + }, + } + }, }, { name: "test List with filter returning two items, more pages possible with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "bar"), Label: labels.Everything(), @@ -1098,17 +1302,32 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "filter returns two items split across multiple pages", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), Limit: 2, }, expectedOut: []example.Pod{*createdPods[2], *createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 0, Limit: 2}, + }, + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 4, Continue: "/registry/pods/second/bar\x00"}, + }, + } + }, }, { name: "filter returns two items split across multiple pages with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), @@ -1120,7 +1339,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "filter returns one item for last page, ends on last item, not full", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), @@ -1128,10 +1347,21 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Continue: encodeContinueOrDie("third/barfoo", int64(continueRV)), }, expectedOut: []example.Pod{*createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 2, Continue: "/registry/pods/third/barfoo"}, + }, + } + }, }, { name: "filter returns one item for last page, starts on last item, full", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), @@ -1139,10 +1369,25 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Continue: encodeContinueOrDie("third/barfoo", int64(continueRV)), }, expectedOut: []example.Pod{*createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 1, Continue: "/registry/pods/third/barfoo"}, + }, + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 2, Continue: "/registry/pods/third/barfoo\x00"}, + }, + } + }, }, { name: "filter returns one item for last page, starts on last item, partial page", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), @@ -1150,20 +1395,42 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Continue: encodeContinueOrDie("third/barfoo", int64(continueRV)), }, expectedOut: []example.Pod{*createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 2, Continue: "/registry/pods/third/barfoo"}, + }, + } + }, }, { name: "filter returns two items, page size equal to total list size", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), Limit: 5, }, expectedOut: []example.Pod{*createdPods[2], *createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 0, Limit: 5}, + }, + } + }, }, { name: "filter returns two items, page size equal to total list size with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "foo"), Label: labels.Everything(), @@ -1175,17 +1442,28 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "filter returns one item, page size equal to total list size", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), Limit: 5, }, expectedOut: []example.Pod{*createdPods[3]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 0, Limit: 5}, + }, + } + }, }, { name: "filter returns one item, page size equal to total list size with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "barfoo"), Label: labels.Everything(), @@ -1197,13 +1475,23 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "list all items", - prefix: "/pods", + prefix: "/pods/", pred: storage.Everything, expectedOut: []example.Pod{*updatedPod, *createdPods[1], *createdPods[2], *createdPods[3], *createdPods[4]}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + }, + } + }, }, { name: "list all items with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.Everything, rv: list.ResourceVersion, rvMatch: metav1.ResourceVersionMatchNotOlderThan, @@ -1211,7 +1499,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "verify list returns updated version of object; filter returns one item, page size equal to total list size with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("spec.nodeName", "fakeNode"), Label: labels.Everything(), @@ -1223,7 +1511,7 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, { name: "verify list does not return deleted object; filter for deleted object, page size equal to total list size with current resource version and match=NotOlderThan", - prefix: "/pods", + prefix: "/pods/", pred: storage.SelectionPredicate{ Field: fields.OneTermEqualSelector("metadata.name", "baz"), Label: labels.Everything(), @@ -1240,6 +1528,16 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: "", expectRV: currentRV, expectedOut: []example.Pod{}, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/empty/", + }, + } + }, }, { name: "test non-consistent List", @@ -1266,6 +1564,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[0].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[0].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 2}, + }, + } + }, }, { name: "test List with resource version of second write, match=Exact", @@ -1275,6 +1584,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[1].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[1].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 3}, + }, + } + }, }, { name: "test List with resource version of third write, match=Exact", @@ -1284,6 +1604,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[2].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[2].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 4}, + }, + } + }, }, { name: "test List with resource version of fourth write, match=Exact", @@ -1293,6 +1624,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[3].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[3].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 5}, + }, + } + }, }, { name: "test List with resource version of fifth write, match=Exact", @@ -1302,6 +1644,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[4].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[4].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 6}, + }, + } + }, }, { name: "test List with resource version of six write, match=Exact", @@ -1311,6 +1664,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: createdPods[5].ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[5].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 7}, + }, + } + }, }, { name: "test List with resource version of seventh write, match=Exact", @@ -1320,6 +1684,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: updatedPod.ResourceVersion, rvMatch: metav1.ResourceVersionMatchExact, expectRV: updatedPod.ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: 8}, + }, + } + }, }, { name: "test List with resource version of eight write, match=Exact", @@ -1329,6 +1704,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: fmt.Sprint(continueRV), rvMatch: metav1.ResourceVersionMatchExact, expectRV: fmt.Sprint(continueRV), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV)}, + }, + } + }, }, { name: "test List with resource version after writes, match=Exact", @@ -1338,6 +1724,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: fmt.Sprint(continueRV + 1), rvMatch: metav1.ResourceVersionMatchExact, expectRV: fmt.Sprint(continueRV + 1), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1}, + }, + } + }, }, { name: "test List with future resource version, match=Exact", @@ -1346,6 +1743,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: fmt.Sprint(continueRV + 2), rvMatch: metav1.ResourceVersionMatchExact, expectRVTooLarge: true, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 2}, + }, + } + }, }, // limit, match=Exact { @@ -1363,6 +1771,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rvMatch: metav1.ResourceVersionMatchExact, expectRV: createdPods[1].ResourceVersion, expectedRemainingItemCount: ptr.To[int64](1), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[1].ResourceVersion), Limit: 1}, + }, + } + }, }, { name: "test List with limit, resource version of third write, match=Exact", @@ -1379,6 +1798,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinueExact: encodeContinueOrDie(createdPods[1].Namespace+"/"+createdPods[1].Name+"\x00", int64(mustAtoi(createdPods[2].ResourceVersion))), expectRV: createdPods[2].ResourceVersion, expectedRemainingItemCount: ptr.To[int64](1), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[2].ResourceVersion), Limit: 2}, + }, + } + }, }, { name: "test List with limit, resource version of fourth write, match=Exact", @@ -1392,6 +1822,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rvMatch: metav1.ResourceVersionMatchExact, expectedOut: []example.Pod{*createdPods[0], *createdPods[1], *createdPods[2], *createdPods[3]}, expectRV: createdPods[3].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[3].ResourceVersion), Limit: 4}, + }, + } + }, }, { name: "test List with limit, resource version of fifth write, match=Exact", @@ -1408,6 +1849,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[0].Namespace+"/"+createdPods[0].Name+"\x00", int64(mustAtoi(createdPods[4].ResourceVersion))), expectedRemainingItemCount: ptr.To[int64](4), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[4].ResourceVersion), Limit: 1}, + }, + } + }, }, { name: "test List with limit, resource version of six write, match=Exact", @@ -1424,6 +1876,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[1].Namespace+"/"+createdPods[1].Name+"\x00", int64(mustAtoi(createdPods[5].ResourceVersion))), expectedRemainingItemCount: ptr.To[int64](4), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[5].ResourceVersion), Limit: 2}, + }, + } + }, }, { name: "test List with limit, resource version of seventh write, match=Exact", @@ -1440,6 +1903,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[2].Namespace+"/"+createdPods[2].Name+"\x00", int64(mustAtoi(updatedPod.ResourceVersion))), expectedRemainingItemCount: ptr.To[int64](2), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, updatedPod.ResourceVersion), Limit: 4}, + }, + } + }, }, { name: "test List with limit, resource version of eight write, match=Exact", @@ -1453,6 +1927,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com rv: fmt.Sprint(continueRV), rvMatch: metav1.ResourceVersionMatchExact, expectRV: fmt.Sprint(continueRV), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV), Limit: 8}, + }, + } + }, }, { name: "test List with limit, resource version after writes, match=Exact", @@ -1469,6 +1954,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(updatedPod.Namespace+"/"+updatedPod.Name+"\x00", int64(continueRV+1)), expectedRemainingItemCount: ptr.To[int64](4), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 1}, + }, + } + }, }, // Continue { @@ -1478,9 +1974,20 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com Label: labels.Everything(), Field: fields.Everything(), Limit: 1, - Continue: encodeContinueOrDie(createdPods[0].Namespace+"/"+createdPods[0].Name+"\x00", int64(initialRV2)), + Continue: encodeContinueOrDie(createdPods[0].Namespace+"/"+createdPods[0].Name+"\x00", mustParseResourceVersion(t, initialRV)), }, expectContinueTooOld: true, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, initialRV), Limit: 4}, + }, + } + }, }, { name: "test List with continue, resource version of first write", @@ -1493,6 +2000,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{}, expectRV: createdPods[0].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[0].ResourceVersion), Limit: 1, Continue: "/registry/pods/first/bar\x00"}, + }, + } + }, }, { name: "test List with continue, resource version of second write", @@ -1505,6 +2023,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[1]}, expectRV: createdPods[1].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[1].ResourceVersion), Limit: 1, Continue: "/registry/pods/first/bar\x00"}, + }, + } + }, }, { name: "test List with continue, resource version of third write", @@ -1517,6 +2046,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[2]}, expectRV: createdPods[2].ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[2].ResourceVersion), Limit: 2, Continue: "/registry/pods/second/bar\x00"}, + }, + } + }, }, { name: "test List with continue, resource version of fifth write", @@ -1532,6 +2072,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[1].Namespace+"/"+createdPods[1].Name+"\x00", int64(mustAtoi(createdPods[4].ResourceVersion))), expectedRemainingItemCount: ptr.To[int64](3), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[4].ResourceVersion), Limit: 1, Continue: "/registry/pods/first/bar\x00"}, + }, + } + }, }, { name: "test List with continue, resource version of six write", @@ -1547,6 +2098,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[2].Namespace+"/"+createdPods[2].Name+"\x00", int64(mustAtoi(createdPods[5].ResourceVersion))), expectedRemainingItemCount: ptr.To[int64](2), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, createdPods[5].ResourceVersion), Limit: 2, Continue: "/registry/pods/second/bar\x00"}, + }, + } + }, }, { name: "test List with continue, resource version of seventh write", @@ -1559,6 +2121,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[3], *createdPods[4]}, expectRV: updatedPod.ResourceVersion, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: mustParseResourceVersion(t, updatedPod.ResourceVersion), Limit: 4, Continue: "/registry/pods/second/foo\x00"}, + }, + } + }, }, { name: "test List with continue, resource version after writes", @@ -1574,6 +2147,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinue: true, expectContinueExact: encodeContinueOrDie(createdPods[1].Namespace+"/"+createdPods[1].Name+"\x00", int64(continueRV+1)), expectedRemainingItemCount: ptr.To[int64](3), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Revision: int64(continueRV) + 1, Limit: 1, Continue: "/registry/pods/first/bar\x00"}, + }, + } + }, }, { name: "test List with continue from second pod, negative resource version gives consistent read", @@ -1585,6 +2169,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[1], *createdPods[2], *createdPods[3], *createdPods[4]}, expectRV: currentRV, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Continue: "/registry/pods/first/bar\x00"}, + }, + } + }, }, { name: "test List with continue from second pod and limit, negative resource version gives consistent read", @@ -1600,6 +2195,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com expectContinueExact: encodeContinueOrDie(createdPods[2].Namespace+"/"+createdPods[2].Name+"\x00", int64(continueRV+1)), expectRV: currentRV, expectedRemainingItemCount: ptr.To[int64](2), + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Continue: "/registry/pods/first/bar\x00", Limit: 2}, + }, + } + }, }, { name: "test List with continue from third pod, negative resource version gives consistent read", @@ -1611,6 +2217,17 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com }, expectedOut: []example.Pod{*createdPods[3], *createdPods[4]}, expectRV: currentRV, + expectCacherRequestsToEtcd: func() []RecordedList { + if utilfeature.DefaultFeatureGate.Enabled(features.ConsistentListFromCache) && utilfeature.DefaultFeatureGate.Enabled(features.ListFromCacheSnapshot) { + return nil + } + return []RecordedList{ + { + Key: "/registry/pods/", + ListOptions: kubernetes.ListOptions{Continue: "/registry/pods/second/foo\x00"}, + }, + } + }, }, { name: "test List with continue from empty fails", @@ -1645,7 +2262,6 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { // For some asynchronous implementations of storage interface (in particular watchcache), // certain requests may impact result of further requests. As an example, if we first @@ -1731,11 +2347,14 @@ func RunTestList(ctx context.Context, t *testing.T, store storage.Interface, com if !cmp.Equal(tt.expectedRemainingItemCount, out.RemainingItemCount) { t.Fatalf("unexpected remainingItemCount, diff: %s", cmp.Diff(tt.expectedRemainingItemCount, out.RemainingItemCount)) } - if watchCacheEnabled && tt.expectEtcdRequest != nil { - expectEtcdLists := tt.expectEtcdRequest() - etcdLists := recorder.ListRequestForKey(recorderKey) - if !cmp.Equal(expectEtcdLists, etcdLists) { - t.Fatalf("unexpected etcd requests, diff: %s", cmp.Diff(expectEtcdLists, etcdLists)) + if watchCacheEnabled { + var expectedListRequests []RecordedList + if tt.expectCacherRequestsToEtcd != nil { + expectedListRequests = tt.expectCacherRequestsToEtcd() + } + gotListRequests := recorder.ListRequestForKey(recorderKey) + if !cmp.Equal(expectedListRequests, gotListRequests) { + t.Fatalf("unexpected etcd list requests, diff: %s", cmp.Diff(expectedListRequests, gotListRequests)) } } }) @@ -1909,7 +2528,7 @@ func seedMultiLevelData(ctx context.Context, store storage.Interface) (initialRV // we want to figure out the resourceVersion before we create anything initialList := &example.PodList{} - if err := store.GetList(ctx, "/pods", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, initialList); err != nil { + if err := store.GetList(ctx, "/pods/", storage.ListOptions{Predicate: storage.Everything, Recursive: true}, initialList); err != nil { return "", nil, nil, fmt.Errorf("failed to determine starting resourceVersion: %w", err) } initialRV = initialList.ResourceVersion @@ -2037,12 +2656,12 @@ func RunTestGetListNonRecursive(ctx context.Context, t *testing.T, increaseRV In expectRVTooLarge: true, }, { name: "non-existing key", - key: "/non-existing", + key: "/pods/non-existing", pred: storage.Everything, expectedOut: []example.Pod{}, }, { name: "with matching pod name", - key: "/non-existing", + key: "/pods/non-existing", pred: storage.SelectionPredicate{ Label: labels.Everything(), Field: fields.ParseSelectorOrDie("metadata.name!=" + storedObj.Name), @@ -2068,7 +2687,6 @@ func RunTestGetListNonRecursive(ctx context.Context, t *testing.T, increaseRV In }} for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { // For some asynchronous implementations of storage interface (in particular watchcache), // certain requests may impact result of further requests. As an example, if we first @@ -2196,10 +2814,8 @@ func RunTestGetListRecursivePrefix(ctx context.Context, t *testing.T, store stor } for _, listType := range listTypes { - listType := listType t.Run(listType.name, func(t *testing.T) { for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { out := &example.PodList{} storageOpts := storage.ListOptions{ @@ -2283,7 +2899,7 @@ func RunTestListContinuation(ctx context.Context, t *testing.T, store storage.In Predicate: pred(1, ""), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get initial list: %v", err) } if len(out.Continue) == 0 { @@ -2307,13 +2923,13 @@ func RunTestListContinuation(ctx context.Context, t *testing.T, store storage.In Predicate: pred(0, continueFromSecondItem), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get second page: %v", err) } if len(out.Continue) != 0 { t.Fatalf("Unexpected continuation token set") } - key, rv, err := storage.DecodeContinue(continueFromSecondItem, "/pods") + key, rv, err := storage.DecodeContinue(continueFromSecondItem, "/pods/") t.Logf("continue token was %d %s %v", rv, key, err) expectNoDiff(t, "incorrect second page", []example.Pod{*preset[1].storedObj, *preset[2].storedObj}, out.Items) if out.ResourceVersion != currentRV { @@ -2331,7 +2947,7 @@ func RunTestListContinuation(ctx context.Context, t *testing.T, store storage.In Predicate: pred(1, continueFromSecondItem), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get second page: %v", err) } if len(out.Continue) == 0 { @@ -2354,7 +2970,7 @@ func RunTestListContinuation(ctx context.Context, t *testing.T, store storage.In Predicate: pred(1, continueFromThirdItem), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get second page: %v", err) } if len(out.Continue) != 0 { @@ -2396,7 +3012,7 @@ func RunTestListPaginationRareObject(ctx context.Context, t *testing.T, store st }, Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get initial list: %v", err) } if len(out.Continue) != 0 { @@ -2472,7 +3088,7 @@ func RunTestListContinuationWithFilter(ctx context.Context, t *testing.T, store Predicate: pred(2, ""), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Errorf("Unable to get initial list: %v", err) } if len(out.Continue) == 0 { @@ -2503,7 +3119,7 @@ func RunTestListContinuationWithFilter(ctx context.Context, t *testing.T, store Predicate: pred(2, cont), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Errorf("Unable to get second page: %v", err) } if len(out.Continue) != 0 { @@ -2519,7 +3135,7 @@ func RunTestListContinuationWithFilter(ctx context.Context, t *testing.T, store } type Compaction func(ctx context.Context, t *testing.T, resourceVersion string) -type IncreaseRVFunc func(ctx context.Context, t *testing.T) +type IncreaseRVFunc func(ctx context.Context, t *testing.T) (revision int64) func RunTestListInconsistentContinuation(ctx context.Context, t *testing.T, store storage.Interface, compaction Compaction) { // Setup storage with the following structure: @@ -2578,7 +3194,7 @@ func RunTestListInconsistentContinuation(ctx context.Context, t *testing.T, stor Predicate: pred(1, ""), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get initial list: %v", err) } if len(out.Continue) == 0 { @@ -2615,7 +3231,7 @@ func RunTestListInconsistentContinuation(ctx context.Context, t *testing.T, stor Predicate: pred(0, continueFromSecondItem), Recursive: true, } - err := store.GetList(ctx, "/pods", options, out) + err := store.GetList(ctx, "/pods/", options, out) if err == nil { t.Fatalf("unexpected no error") } @@ -2637,7 +3253,7 @@ func RunTestListInconsistentContinuation(ctx context.Context, t *testing.T, stor Predicate: pred(1, inconsistentContinueFromSecondItem), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get second page: %v", err) } if len(out.Continue) == 0 { @@ -2656,7 +3272,7 @@ func RunTestListInconsistentContinuation(ctx context.Context, t *testing.T, stor Predicate: pred(1, continueFromThirdItem), Recursive: true, } - if err := store.GetList(ctx, "/pods", options, out); err != nil { + if err := store.GetList(ctx, "/pods/", options, out); err != nil { t.Fatalf("Unable to get second page: %v", err) } if len(out.Continue) != 0 { @@ -2730,7 +3346,7 @@ func RunTestListResourceVersionMatch(ctx context.Context, t *testing.T, store In Predicate: predicate, Recursive: true, } - if err := store.GetList(ctx, "/pods", options, &result1); err != nil { + if err := store.GetList(ctx, "/pods/", options, &result1); err != nil { t.Fatalf("failed to list objects: %v", err) } @@ -2743,7 +3359,7 @@ func RunTestListResourceVersionMatch(ctx context.Context, t *testing.T, store In } result2 := example.PodList{} - if err := store.GetList(ctx, "/pods", options, &result2); err != nil { + if err := store.GetList(ctx, "/pods/", options, &result2); err != nil { t.Fatalf("failed to list objects: %v", err) } @@ -2753,7 +3369,7 @@ func RunTestListResourceVersionMatch(ctx context.Context, t *testing.T, store In options.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan result3 := example.PodList{} - if err := store.GetList(ctx, "/pods", options, &result3); err != nil { + if err := store.GetList(ctx, "/pods/", options, &result3); err != nil { t.Fatalf("failed to list objects: %v", err) } @@ -2761,7 +3377,7 @@ func RunTestListResourceVersionMatch(ctx context.Context, t *testing.T, store In options.ResourceVersionMatch = metav1.ResourceVersionMatchExact result4 := example.PodList{} - if err := store.GetList(ctx, "/pods", options, &result4); err != nil { + if err := store.GetList(ctx, "/pods/", options, &result4); err != nil { t.Fatalf("failed to list objects: %v", err) } @@ -2784,7 +3400,7 @@ func RunTestGuaranteedUpdate(ctx context.Context, t *testing.T, store InterfaceW hasSelfLink bool }{{ name: "non-existing key, ignoreNotFound=false", - key: "/non-existing", + key: "/pods/non-existing", ignoreNotFound: false, precondition: nil, expectNotFoundErr: true, @@ -2792,7 +3408,7 @@ func RunTestGuaranteedUpdate(ctx context.Context, t *testing.T, store InterfaceW expectNoUpdate: false, }, { name: "non-existing key, ignoreNotFound=true", - key: "/non-existing", + key: "/pods/non-existing", ignoreNotFound: true, precondition: nil, expectNotFoundErr: false, @@ -3097,7 +3713,7 @@ func RunTestGuaranteedUpdateWithSuggestionAndConflict(ctx context.Context, t *te t.Fatalf("unexpected error: %v", err) } if updatedPod2.Generation != 3 { - t.Errorf("unexpected pod generation: %q", updatedPod2.Generation) + t.Errorf("unexpected pod generation: %d", updatedPod2.Generation) } // Third, update using a current version as the suggestion. @@ -3175,7 +3791,7 @@ func RunTestTransformationFailure(ctx context.Context, t *testing.T, store Inter Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/pods", storageOpts, &got); !storage.IsInternalError(err) { + if err := store.GetList(ctx, "/pods/", storageOpts, &got); !storage.IsInternalError(err) { t.Errorf("Unexpected error %v", err) } @@ -3256,7 +3872,7 @@ func RunTestStats(ctx context.Context, t *testing.T, store storage.Interface, co func assertStats(t *testing.T, store storage.Interface, sizeEnabled bool, expectStats storage.Stats) { t.Helper() // Execute consistent LIST to refresh state of cache. - err := store.GetList(t.Context(), "/pods", storage.ListOptions{Recursive: true, Predicate: storage.Everything}, &example.PodList{}) + err := store.GetList(t.Context(), "/pods/", storage.ListOptions{Recursive: true, Predicate: storage.Everything}, &example.PodList{}) if err != nil { t.Fatalf("GetList failed: %v", err) } @@ -3309,7 +3925,7 @@ func RunTestListPaging(ctx context.Context, t *testing.T, store storage.Interfac for { calls++ listOut := &example.PodList{} - err := store.GetList(ctx, "/pods", opts, listOut) + err := store.GetList(ctx, "/pods/", opts, listOut) if err != nil { t.Fatalf("Unexpected error %s", err) } @@ -3739,7 +4355,7 @@ func RunTestNamespaceScopedList(ctx context.Context, t *testing.T, store storage func RunTestCompactRevision(ctx context.Context, t *testing.T, store storage.Interface, increaseRV IncreaseRVFunc, compact Compaction) { listOut := &example.PodList{} - if err := store.GetList(ctx, "/pods", storage.ListOptions{ + if err := store.GetList(ctx, "/pods/", storage.ListOptions{ Predicate: storage.Everything, Recursive: true, }, listOut); err != nil { @@ -3753,7 +4369,7 @@ func RunTestCompactRevision(ctx context.Context, t *testing.T, store storage.Int if compactedRV >= int64(currentRV) { t.Fatalf("Expected current RV not be compacted, current: %d, compacted: %d", currentRV, compactedRV) } - if err := store.GetList(ctx, "/pods", storage.ListOptions{ + if err := store.GetList(ctx, "/pods/", storage.ListOptions{ Predicate: storage.Everything, Recursive: true, ResourceVersion: listOut.ResourceVersion, @@ -3764,7 +4380,7 @@ func RunTestCompactRevision(ctx context.Context, t *testing.T, store storage.Int increaseRV(ctx, t) expectCompactedRV := currentRV + 1 compact(ctx, t, fmt.Sprintf("%d", expectCompactedRV)) - if err := store.GetList(ctx, "/pods", storage.ListOptions{ + if err := store.GetList(ctx, "/pods/", storage.ListOptions{ Predicate: storage.Everything, Recursive: true, ResourceVersion: listOut.ResourceVersion, @@ -3777,3 +4393,122 @@ func RunTestCompactRevision(ctx context.Context, t *testing.T, store storage.Int t.Errorf("CompactRevision()=%d, expected: %d", store.CompactRevision(), expectCompactedRV) } } + +func RunTestKeySchema(ctx context.Context, t *testing.T, store storage.Interface) { + createObj := &example.Pod{} + createOut := &example.Pod{} + require.ErrorContains(t, store.Create(ctx, "", createObj, createOut, 0), "empty key") + require.ErrorContains(t, store.Create(ctx, "/", createObj, createOut, 0), "empty key") + require.ErrorContains(t, store.Create(ctx, ".", createObj, createOut, 0), "invalid key") + require.ErrorContains(t, store.Create(ctx, "..", createObj, createOut, 0), "invalid key") + require.ErrorContains(t, store.Create(ctx, "pods", createObj, createOut, 0), "lacks resource prefix") + require.ErrorContains(t, store.Create(ctx, "/pods", createObj, createOut, 0), "lacks resource prefix") + require.ErrorContains(t, store.Create(ctx, "/pods.apps", createObj, createOut, 0), "lacks resource prefix") + require.ErrorContains(t, store.Create(ctx, "/foo/", createObj, createOut, 0), "lacks resource prefix") + require.NoError(t, store.Create(ctx, "/pods/", createObj, createOut, 0)) + require.NoError(t, store.Create(ctx, "/pods/name", createObj, createOut, 0)) + require.NoError(t, store.Create(ctx, "/pods/namespace", createObj, createOut, 0)) + require.NoError(t, store.Create(ctx, "/pods/namespace/name", createObj, createOut, 0)) + + listOut := &example.PodList{} + recursiveListOpts := storage.ListOptions{Predicate: storage.Everything, Recursive: true} + nonRecursiveListOpts := storage.ListOptions{Predicate: storage.Everything, Recursive: false} + require.ErrorContains(t, store.GetList(ctx, "", recursiveListOpts, listOut), "empty key") + require.ErrorContains(t, store.GetList(ctx, "/", recursiveListOpts, listOut), "empty key") + require.ErrorContains(t, store.GetList(ctx, ".", recursiveListOpts, listOut), "invalid key") + require.ErrorContains(t, store.GetList(ctx, "..", recursiveListOpts, listOut), "invalid key") + require.ErrorContains(t, store.GetList(ctx, "pods", recursiveListOpts, listOut), "lacks resource prefix") + require.ErrorContains(t, store.GetList(ctx, "/pods.apps", recursiveListOpts, listOut), "lacks resource prefix") + require.ErrorContains(t, store.GetList(ctx, "/foo/", recursiveListOpts, listOut), "lacks resource prefix") + require.ErrorContains(t, store.GetList(ctx, "/pods", nonRecursiveListOpts, listOut), "lacks resource prefix") + require.NoError(t, store.GetList(ctx, "/pods", recursiveListOpts, listOut)) + require.NoError(t, store.GetList(ctx, "/pods/", recursiveListOpts, listOut)) + require.NoError(t, store.GetList(ctx, "/pods/namespace", recursiveListOpts, listOut)) + require.NoError(t, store.GetList(ctx, "/pods/namespace/name", recursiveListOpts, listOut)) + + getOut := &example.Pod{} + getOpts := storage.GetOptions{} + require.ErrorContains(t, store.Get(ctx, "", getOpts, getOut), "empty key") + require.ErrorContains(t, store.Get(ctx, "/", getOpts, getOut), "empty key") + require.ErrorContains(t, store.Get(ctx, ".", getOpts, getOut), "invalid key") + require.ErrorContains(t, store.Get(ctx, "..", getOpts, getOut), "invalid key") + require.ErrorContains(t, store.Get(ctx, "pods", getOpts, getOut), "lacks resource prefix") + require.ErrorContains(t, store.Get(ctx, "/pods", getOpts, getOut), "lacks resource prefix") + require.ErrorContains(t, store.Get(ctx, "/pods.apps", getOpts, getOut), "lacks resource prefix") + require.ErrorContains(t, store.Get(ctx, "/foo/", getOpts, getOut), "lacks resource prefix") + require.NoError(t, store.Get(ctx, "/pods/", getOpts, getOut)) + require.NoError(t, store.Get(ctx, "/pods/namespace", getOpts, getOut)) + require.NoError(t, store.Get(ctx, "/pods/namespace/name", getOpts, getOut)) + + _, err := store.Watch(ctx, "", recursiveListOpts) + require.ErrorContains(t, err, "empty key") + _, err = store.Watch(ctx, "/", recursiveListOpts) + require.ErrorContains(t, err, "empty key") + _, err = store.Watch(ctx, ".", recursiveListOpts) + require.ErrorContains(t, err, "invalid key") + _, err = store.Watch(ctx, "..", recursiveListOpts) + require.ErrorContains(t, err, "invalid key") + _, err = store.Watch(ctx, "pods", recursiveListOpts) + require.ErrorContains(t, err, "lacks resource prefix") + _, err = store.Watch(ctx, "/pods.apps", recursiveListOpts) + require.ErrorContains(t, err, "lacks resource prefix") + _, err = store.Watch(ctx, "/foo/", recursiveListOpts) + require.ErrorContains(t, err, "lacks resource prefix") + _, err = store.Watch(ctx, "/pods", nonRecursiveListOpts) + require.ErrorContains(t, err, "lacks resource prefix") + w, err := store.Watch(ctx, "/pods", recursiveListOpts) + require.NoError(t, err) + w.Stop() + require.NoError(t, err) + w.Stop() + w, err = store.Watch(ctx, "/pods/namespace", recursiveListOpts) + require.NoError(t, err) + w.Stop() + w, err = store.Watch(ctx, "/pods/namespace/name", recursiveListOpts) + require.NoError(t, err) + w.Stop() + + updateIn := &example.Pod{} + updateOut := &example.Pod{} + updateFunc := func(input runtime.Object, res storage.ResponseMeta) (output runtime.Object, ttl *uint64, err error) { + return updateIn, nil, nil + } + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "", updateOut, false, nil, updateFunc, nil), "empty key") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "/", updateOut, false, nil, updateFunc, nil), "empty key") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, ".", updateOut, false, nil, updateFunc, nil), "invalid key") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "..", updateOut, false, nil, updateFunc, nil), "invalid key") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "pods", updateOut, false, nil, updateFunc, nil), "lacks resource prefix") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "/pods", updateOut, false, nil, updateFunc, nil), "lacks resource prefix") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "/pods.apps", updateOut, false, nil, updateFunc, nil), "lacks resource prefix") + require.ErrorContains(t, store.GuaranteedUpdate(ctx, "/foo/", updateOut, false, nil, updateFunc, nil), "lacks resource prefix") + require.NoError(t, store.GuaranteedUpdate(ctx, "/pods/", updateOut, false, nil, updateFunc, nil)) + require.NoError(t, store.GuaranteedUpdate(ctx, "/pods/namespace", updateOut, false, nil, updateFunc, nil)) + require.NoError(t, store.GuaranteedUpdate(ctx, "/pods/namespace/name", updateOut, false, nil, updateFunc, nil)) + + deleteOut := &example.Pod{} + deleteFunc := func(ctx context.Context, obj runtime.Object) error { + return nil + } + deleteOpts := storage.DeleteOptions{} + require.ErrorContains(t, store.Delete(ctx, "", deleteOut, nil, deleteFunc, nil, deleteOpts), "empty key") + require.ErrorContains(t, store.Delete(ctx, "/", deleteOut, nil, deleteFunc, nil, deleteOpts), "empty key") + require.ErrorContains(t, store.Delete(ctx, ".", deleteOut, nil, deleteFunc, nil, deleteOpts), "invalid key") + require.ErrorContains(t, store.Delete(ctx, "..", deleteOut, nil, deleteFunc, nil, deleteOpts), "invalid key") + require.ErrorContains(t, store.Delete(ctx, "pods", deleteOut, nil, deleteFunc, nil, deleteOpts), "lacks resource prefix") + require.ErrorContains(t, store.Delete(ctx, "/pods", deleteOut, nil, deleteFunc, nil, deleteOpts), "lacks resource prefix") + require.ErrorContains(t, store.Delete(ctx, "/pods.apps", deleteOut, nil, deleteFunc, nil, deleteOpts), "lacks resource prefix") + require.ErrorContains(t, store.Delete(ctx, "/foo", deleteOut, nil, deleteFunc, nil, deleteOpts), "lacks resource prefix") + require.NoError(t, store.Delete(ctx, "/pods/", deleteOut, nil, deleteFunc, nil, deleteOpts)) + require.NoError(t, store.Delete(ctx, "/pods/namespace", deleteOut, nil, deleteFunc, nil, deleteOpts)) + require.NoError(t, store.Delete(ctx, "/pods/namespace/name", deleteOut, nil, deleteFunc, nil, deleteOpts)) +} + +func mustParseResourceVersion(t *testing.T, resourceVersion string) int64 { + t.Helper() + versioner := storage.APIObjectVersioner{} + rv, err := versioner.ParseResourceVersion(resourceVersion) + if err != nil { + t.Fatal(err) + } + return int64(rv) +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/utils.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/utils.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/utils.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/utils.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/watcher_tests.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/watcher_tests.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/watcher_tests.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/watcher_tests.go index 567e24755..e57a99d3d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testing/watcher_tests.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testing/watcher_tests.go @@ -347,9 +347,6 @@ func RunTestDelayedWatchDelivery(ctx context.Context, t *testing.T, store storag } } - // Now stop the watcher and check if the consecutive events are being delivered. - watcher.Stop() - watched := 0 for { event, ok := <-watcher.ResultChan() @@ -364,11 +361,19 @@ func RunTestDelayedWatchDelivery(ctx context.Context, t *testing.T, store storag t.Errorf("Unexpected object watched: %s, expected %s", a, e) } watched++ + // Before stopping watcher wait for an event to arrive and give them some time to fill the queue. + if watched == 1 { + time.Sleep(time.Second) + // Stop the watcher to check if the consecutive events will be delivered. + watcher.Stop() + } } // We expect at least N events to be delivered, depending on the implementation. // For now, this number is smallest for Cacher and it equals 10 (size of the out buffer). - if watched < 10 { - t.Errorf("Unexpected number of events: %v, expected: %v", watched, totalPods) + outBufferSize := 10 + expectWatched := outBufferSize + 1 // initial event + if watched < expectWatched { + t.Errorf("Unexpected number of events: %v, expected at least: %v", watched, expectWatched) } } @@ -383,7 +388,7 @@ func RunTestWatchError(ctx context.Context, t *testing.T, store InterfaceWithPre Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/pods", storageOpts, list); err != nil { + if err := store.GetList(ctx, "/pods/", storageOpts, list); err != nil { t.Errorf("Unexpected error: %v", err) } @@ -424,7 +429,7 @@ func RunTestWatchWithUnsafeDelete(ctx context.Context, t *testing.T, store Inter Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/pods", storageOpts, list); err != nil { + if err := store.GetList(ctx, "/pods/", storageOpts, list); err != nil { t.Errorf("Unexpected error: %v", err) } @@ -493,7 +498,7 @@ func RunTestWatcherTimeout(ctx context.Context, t *testing.T, store storage.Inte Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/pods", options, &podList); err != nil { + if err := store.GetList(ctx, "/pods/", options, &podList); err != nil { t.Fatalf("Failed to list pods: %v", err) } initialRV := podList.ResourceVersion @@ -734,9 +739,9 @@ func RunTestClusterScopedWatch(ctx context.Context, t *testing.T, store storage. ctx = genericapirequest.WithRequestInfo(ctx, requestInfo) ctx = genericapirequest.WithNamespace(ctx, "") - watchKey := "/pods" + watchKey := "/pods/" if tt.requestedName != "" { - watchKey += "/" + tt.requestedName + watchKey += tt.requestedName } predicate := CreatePodPredicate(tt.fieldSelector, false, tt.indexFields) @@ -747,7 +752,7 @@ func RunTestClusterScopedWatch(ctx context.Context, t *testing.T, store storage. Predicate: predicate, Recursive: true, } - if err := store.GetList(ctx, "/pods", opts, list); err != nil { + if err := store.GetList(ctx, "/pods/", opts, list); err != nil { t.Errorf("Unexpected error: %v", err) } @@ -762,7 +767,7 @@ func RunTestClusterScopedWatch(ctx context.Context, t *testing.T, store storage. currentObjs := map[string]*example.Pod{} for _, watchTest := range tt.watchTests { out := &example.Pod{} - key := "pods/" + watchTest.obj.Name + key := "/pods/" + watchTest.obj.Name err := store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { obj := watchTest.obj.DeepCopy() @@ -1045,11 +1050,11 @@ func RunTestNamespaceScopedWatch(ctx context.Context, t *testing.T, store storag ctx = genericapirequest.WithRequestInfo(ctx, requestInfo) ctx = genericapirequest.WithNamespace(ctx, tt.requestedNamespace) - watchKey := "/pods" + watchKey := "/pods/" if tt.requestedNamespace != "" { - watchKey += "/" + tt.requestedNamespace + watchKey += tt.requestedNamespace + "/" if tt.requestedName != "" { - watchKey += "/" + tt.requestedName + watchKey += tt.requestedName } } @@ -1061,7 +1066,7 @@ func RunTestNamespaceScopedWatch(ctx context.Context, t *testing.T, store storag Predicate: predicate, Recursive: true, } - if err := store.GetList(ctx, "/pods", opts, list); err != nil { + if err := store.GetList(ctx, "/pods/", opts, list); err != nil { t.Errorf("Unexpected error: %v", err) } @@ -1076,7 +1081,7 @@ func RunTestNamespaceScopedWatch(ctx context.Context, t *testing.T, store storag currentObjs := map[string]*example.Pod{} for _, watchTest := range tt.watchTests { out := &example.Pod{} - key := "pods/" + watchTest.obj.Namespace + "/" + watchTest.obj.Name + key := "/pods/" + watchTest.obj.Namespace + "/" + watchTest.obj.Name err := store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate( func(runtime.Object) (runtime.Object, error) { obj := watchTest.obj.DeepCopy() @@ -1189,7 +1194,7 @@ func RunTestOptionalWatchBookmarksWithCorrectResourceVersion(ctx context.Context Predicate: storage.Everything, Recursive: true, } - if err := store.GetList(ctx, "/pods", storageOpts, list); err != nil { + if err := store.GetList(ctx, "/pods/", storageOpts, list); err != nil { t.Errorf("Unexpected error: %v", err) } startRV := list.ResourceVersion @@ -1283,7 +1288,7 @@ func RunTestOptionalWatchBookmarksWithCorrectResourceVersion(ctx context.Context func RunSendInitialEventsBackwardCompatibility(ctx context.Context, t *testing.T, store storage.Interface) { opts := storage.ListOptions{Predicate: storage.Everything} opts.SendInitialEvents = ptr.To(true) - w, err := store.Watch(ctx, "/pods", opts) + w, err := store.Watch(ctx, "/pods/", opts) require.NoError(t, err) w.Stop() } @@ -1697,7 +1702,7 @@ func RunWatchListMatchSingle(ctx context.Context, t *testing.T, store storage.In opts.SendInitialEvents = &trueVal opts.Predicate.AllowWatchBookmarks = true - w, err := store.Watch(context.Background(), "/pods", opts) + w, err := store.Watch(context.Background(), "/pods/", opts) require.NoError(t, err, "failed to create watch: %v") defer w.Stop() @@ -1750,7 +1755,7 @@ func RunWatchErrorIsBlockingFurtherEvents(ctx context.Context, t *testing.T, sto wg.Add(1) go func() { defer wg.Done() - w, err := store.Watch(ctx, "/pods", opts) + w, err := store.Watch(ctx, "/pods/", opts) if err != nil { t.Errorf("failed to create watch: %v", err) return diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/zz_generated.deepcopy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/zz_generated.deepcopy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/testresource/zz_generated.deepcopy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/testresource/zz_generated.deepcopy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/util.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/util.go similarity index 86% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/util.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/util.go index bb231df30..941422ef5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/util.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/util.go @@ -18,10 +18,11 @@ package storage import ( "fmt" + "strings" "sync/atomic" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/api/validate/content" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -37,27 +38,33 @@ func SimpleUpdate(fn SimpleUpdateFunc) UpdateFunc { } func NamespaceKeyFunc(prefix string, obj runtime.Object) (string, error) { + if !strings.HasSuffix(prefix, "/") { + return "", fmt.Errorf("prefix should have '/' suffix") + } meta, err := meta.Accessor(obj) if err != nil { return "", err } name := meta.GetName() - if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 { + if msgs := content.IsPathSegmentName(name); len(msgs) != 0 { return "", fmt.Errorf("invalid name: %v", msgs) } - return prefix + "/" + meta.GetNamespace() + "/" + name, nil + return prefix + meta.GetNamespace() + "/" + name, nil } func NoNamespaceKeyFunc(prefix string, obj runtime.Object) (string, error) { + if !strings.HasSuffix(prefix, "/") { + return "", fmt.Errorf("prefix should have '/' suffix") + } meta, err := meta.Accessor(obj) if err != nil { return "", err } name := meta.GetName() - if msgs := path.IsValidPathSegmentName(name); len(msgs) != 0 { + if msgs := content.IsPathSegmentName(name); len(msgs) != 0 { return "", fmt.Errorf("invalid name: %v", msgs) } - return prefix + "/" + name, nil + return prefix + name, nil } // HighWaterMark is a thread-safe object for tracking the maximum value seen diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/util_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/util_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/util_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/util_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes_extended_nonce.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes_extended_nonce.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes_extended_nonce.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes_extended_nonce.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes_test.go index 65c10bdd0..37b45013f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/aes_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/aes_test.go @@ -502,7 +502,6 @@ func BenchmarkGCMRead(b *testing.B) { name := fmt.Sprintf("%vKeyLength/%vValueLength/%vExpectStale", t.keyLength, t.valueLength, t.expectStale) b.Run(name, func(b *testing.B) { for _, n := range gcmBenchmarks { - n := n if t.keyLength == 16 && n.name == "gcm-extended-nonce" { continue // gcm-extended-nonce requires 32 byte keys } @@ -528,7 +527,6 @@ func BenchmarkGCMWrite(b *testing.B) { name := fmt.Sprintf("%vKeyLength/%vValueLength", t.keyLength, t.valueLength) b.Run(name, func(b *testing.B) { for _, n := range gcmBenchmarks { - n := n if t.keyLength == 16 && n.name == "gcm-extended-nonce" { continue // gcm-extended-nonce requires 32 byte keys } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/cache.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/cache.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/cache.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/cache.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/cache_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/cache_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/cache_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/cache_test.go index c8ba95829..718064240 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/aes/cache_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/aes/cache_test.go @@ -110,7 +110,6 @@ func Test_simpleCache(t *testing.T) { }, } for _, tt := range tests { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() clock := clocktesting.NewFakeClock(time.Now()) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/envelope.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/envelope.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/envelope.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/envelope.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/envelope_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/envelope_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/envelope_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/envelope_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service.go index b2a5fd145..a5987c789 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service.go @@ -86,7 +86,7 @@ func NewGRPCService(ctx context.Context, endpoint string, callTimeout time.Durat s.kmsClient = kmsapi.NewKeyManagementServiceClient(s.connection) go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) <-ctx.Done() _ = s.connection.Close() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go index ede17d23e..96365df0a 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/grpc_service_unix_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2017 The Kubernetes Authors. @@ -112,7 +111,6 @@ func TestTimeouts(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() var ( diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/cache.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/cache.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/cache.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/cache.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/cache_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/cache_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/cache_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/cache_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go similarity index 94% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go index b12f8a3d2..01947eb05 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope.go @@ -24,6 +24,7 @@ import ( "crypto/sha256" "fmt" "sort" + "sync" "time" "unsafe" @@ -51,6 +52,29 @@ func init() { metrics.RegisterMetrics() } +// nowFuncPerKMS allows us to swap the NowFunc for KMS providers in tests +// Note: it cannot be set by an end user +var nowFuncPerKMS sync.Map // map[string]func() time.Time, KMS name -> NowFunc + +// SetNowFuncForTests should only be called in tests to swap the NowFunc for KMS providers +// Caller must guarantee that all KMS providers have distinct names across all tests. +func SetNowFuncForTests(kmsName string, fn func() time.Time) func() { + if len(kmsName) == 0 { // guarantee that GetNowFunc("") returns the default value + panic("empty KMS name used in test") + } + nowFuncPerKMS.Store(kmsName, fn) + return func() { nowFuncPerKMS.Delete(kmsName) } +} + +// GetNowFunc returns the time function for the given KMS provider name +func GetNowFunc(kmsName string) func() time.Time { + nowFunc, ok := nowFuncPerKMS.Load(kmsName) + if !ok { + return time.Now + } + return nowFunc.(func() time.Time) +} + const ( // KMSAPIVersionv2 is a version of the KMS API. KMSAPIVersionv2 = "v2" @@ -81,9 +105,6 @@ const ( errKeyIDTooLongCode ErrCodeKeyID = "too_long" ) -// NowFunc is exported so tests can override it. -var NowFunc = time.Now - type StateFunc func() (State, error) type ErrCodeKeyID string @@ -101,10 +122,14 @@ type State struct { // CacheKey is the key used to cache the DEK/seed in envelopeTransformer.cache. CacheKey []byte + + // KMSProviderName is used to dynamically look up the time function for tests + KMSProviderName string } func (s *State) ValidateEncryptCapability() error { - if now := NowFunc(); now.After(s.ExpirationTimestamp) { + nowFunc := GetNowFunc(s.KMSProviderName) + if now := nowFunc(); now.After(s.ExpirationTimestamp) { return fmt.Errorf("encryptedDEKSource with keyID hash %q expired at %s (current time is %s)", GetHashIfNotEmpty(s.EncryptedObjectKeyID), s.ExpirationTimestamp.Format(time.RFC3339), now.Format(time.RFC3339)) } @@ -498,7 +523,6 @@ func generateCacheKey(encryptedDEKSourceType kmstypes.EncryptedDEKSourceType, en // Sort the annotations by key. keys := make([]string, 0, len(annotations)) for k := range annotations { - k := k keys = append(keys, k) } sort.Strings(keys) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go index 59dae5b19..a70c09e54 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/envelope_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2022 The Kubernetes Authors. @@ -315,7 +314,6 @@ func TestEnvelopeTransformerStaleness(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() @@ -513,7 +511,6 @@ func TestTransformToStorageError(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() @@ -659,7 +656,6 @@ func TestValidateAnnotations(t *testing.T) { } t.Run("success", func(t *testing.T) { for i := range successCases { - i := i t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Parallel() if err := validateAnnotations(successCases[i]); err != nil { @@ -697,7 +693,6 @@ func TestValidateAnnotations(t *testing.T) { t.Run("name error", func(t *testing.T) { for i := range annotationsNameErrorCases { - i := i t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Parallel() err := validateAnnotations(annotationsNameErrorCases[i].annotations) @@ -725,7 +720,6 @@ func TestValidateAnnotations(t *testing.T) { } t.Run("size error", func(t *testing.T) { for i := range annotationsSizeErrorCases { - i := i t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Parallel() err := validateAnnotations(annotationsSizeErrorCases[i].annotations) @@ -770,7 +764,6 @@ func TestValidateKeyID(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() errCode, err := ValidateKeyID(tt.keyID) @@ -823,7 +816,6 @@ func TestValidateEncryptedDEKSource(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() err := validateEncryptedDEKSource(tt.encryptedDEKSource) @@ -1029,7 +1021,6 @@ func TestEnvelopeLogging(t *testing.T) { } for _, tc := range testCases { - tc := tc t.Run(tc.desc, func(t *testing.T) { var buf bytes.Buffer klog.SetOutput(&buf) @@ -1171,9 +1162,7 @@ func TestGenerateCacheKey(t *testing.T) { } for _, tc := range testCases { - tc := tc for _, tc2 := range testCases { - tc2 := tc2 t.Run(fmt.Sprintf("%+v-%+v", tc, tc2), func(t *testing.T) { key1, err1 := generateCacheKey(tc.encryptedDEKSourceType, tc.encryptedDEKSource, tc.keyID, tc.annotations) key2, err2 := generateCacheKey(tc2.encryptedDEKSourceType, tc2.encryptedDEKSource, tc2.keyID, tc2.annotations) @@ -1241,7 +1230,6 @@ func TestGenerateTransformer(t *testing.T) { } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go index 09a2a76df..7cd7d1ce2 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service.go @@ -83,7 +83,7 @@ func NewGRPCService(ctx context.Context, endpoint, providerName string, callTime s.kmsClient = kmsapi.NewKeyManagementServiceClient(s.connection) go func() { - defer utilruntime.HandleCrash() + defer utilruntime.HandleCrashWithContext(ctx) <-ctx.Done() _ = s.connection.Close() @@ -145,9 +145,10 @@ func (g *gRPCService) Status(ctx context.Context) (*kmsservice.StatusResponse, e func recordMetricsInterceptor(providerName string) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - start := NowFunc() + nowFunc := GetNowFunc(providerName) + start := nowFunc() respErr := invoker(ctx, method, req, reply, cc, opts...) - elapsed := NowFunc().Sub(start) + elapsed := nowFunc().Sub(start) metrics.RecordKMSOperationLatency(providerName, method, elapsed, respErr) return respErr } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go index c0a78d993..6826b18e9 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/grpc_service_unix_test.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2022 The Kubernetes Authors. @@ -118,7 +117,6 @@ func TestTimeouts(t *testing.T) { } for _, tt := range testCases { - tt := tt t.Run(tt.desc, func(t *testing.T) { t.Parallel() var ( diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.pb.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.pb.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.pb.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.pb.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.proto b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.proto similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.proto rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/api.proto diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/v2.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/v2.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/v2.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/kmsv2/v2/v2.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go index 595107a89..445bb6a53 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v1beta1/kms_plugin_mock.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2017 The Kubernetes Authors. @@ -69,6 +68,8 @@ func NewBase64Plugin(t *testing.T, socketPath string) *Base64Plugin { socketPath: socketPath, } + // Make sure we don't have a leftover socket. + _ = os.Remove(socketPath) kmsapi.RegisterKeyManagementServiceServer(server, result) if err := result.start(); err != nil { t.Fatalf("failed to start KMS plugin, err: %v", err) diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go index d5da68f9a..823805c76 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/envelope/testing/v2/kms_plugin_mock.go @@ -1,5 +1,4 @@ //go:build !windows -// +build !windows /* Copyright 2022 The Kubernetes Authors. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/identity/identity.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/identity/identity.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/identity/identity.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/identity/identity.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/secretbox/secretbox.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/secretbox/secretbox.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/secretbox/secretbox.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/secretbox/secretbox.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/secretbox/secretbox_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/secretbox/secretbox_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/encrypt/secretbox/secretbox_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/encrypt/secretbox/secretbox_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/transformer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/transformer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/transformer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/transformer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/transformer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/transformer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storage/value/transformer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storage/value/transformer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/manager.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/manager.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/manager.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/manager_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/manager_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/manager_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/updater.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/updater.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/updater.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/updater.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/updater_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/updater_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/storageversion/updater_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/storageversion/updater_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/apihelpers/helpers.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/apihelpers/helpers.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/apihelpers/helpers.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/apihelpers/helpers.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/registry.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/registry.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/registry.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/registry.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/version.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/version.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/version.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/version.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/version_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/version_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/compatibility/version_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/compatibility/version_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/configmetrics/info_collector.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/configmetrics/info_collector.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/configmetrics/info_collector.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/configmetrics/info_collector.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/configmetrics/info_collector_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/configmetrics/info_collector_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/configmetrics/info_collector_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/configmetrics/info_collector_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/dryrun/dryrun.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/dryrun/dryrun.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/dryrun/dryrun.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/dryrun/dryrun.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/feature/feature_gate.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/feature/feature_gate.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/feature/feature_gate.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/feature/feature_gate.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil.go new file mode 100644 index 000000000..fe594e4bf --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil.go @@ -0,0 +1,149 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesystem + +import ( + "context" + "fmt" + "time" + + "github.com/fsnotify/fsnotify" +) + +type watchAddRemover interface { + Add(path string) error + Remove(path string) error +} + +type noopWatcher struct{} + +func (noopWatcher) Add(path string) error { return nil } +func (noopWatcher) Remove(path string) error { return nil } + +// WatchUntil watches the specified path for changes and blocks until ctx is canceled. +// eventHandler() must be non-nil, and pollInterval must be greater than 0. +// eventHandler() is invoked whenever a change event is observed or pollInterval elapses. +// errorHandler() is invoked (if non-nil) whenever an error occurs initializing or watching the specified path. +// +// If path is a directory, only the directory and immediate children are watched. +// +// If path does not exist or cannot be watched, an error is passed to errorHandler() and eventHandler() is called at pollInterval. +// +// Multiple observed events may collapse to a single invocation of eventHandler(). +// +// eventHandler() is invoked immediately after successful initialization of the filesystem watch, +// in case the path changed concurrent with calling WatchUntil(). +func WatchUntil(ctx context.Context, pollInterval time.Duration, path string, eventHandler func(), errorHandler func(err error)) { + if pollInterval <= 0 { + panic(fmt.Errorf("pollInterval must be > 0")) + } + if eventHandler == nil { + panic(fmt.Errorf("eventHandler must be non-nil")) + } + if errorHandler == nil { + errorHandler = func(err error) {} + } + + // Initialize watcher, fall back to no-op + var ( + eventsCh chan fsnotify.Event + errorCh chan error + watcher watchAddRemover + ) + if w, err := fsnotify.NewWatcher(); err != nil { + errorHandler(fmt.Errorf("error creating file watcher, falling back to poll at interval %s: %w", pollInterval, err)) + watcher = noopWatcher{} + } else { + watcher = w + eventsCh = w.Events + errorCh = w.Errors + defer func() { + _ = w.Close() + }() + } + + // Initialize background poll + t := time.NewTicker(pollInterval) + defer t.Stop() + + attemptPeriodicRewatch := false + + // Start watching the path + if err := watcher.Add(path); err != nil { + errorHandler(err) + attemptPeriodicRewatch = true + } else { + // Invoke handle() at least once after successfully registering the listener, + // in case the file changed concurrent with calling WatchUntil. + eventHandler() + } + + for { + select { + case <-ctx.Done(): + return + + case <-t.C: + // Prioritize exiting if context is canceled + if ctx.Err() != nil { + return + } + + // Try to re-establish the watcher if we previously got a watch error + if attemptPeriodicRewatch { + _ = watcher.Remove(path) + if err := watcher.Add(path); err != nil { + errorHandler(err) + } else { + attemptPeriodicRewatch = false + } + } + + // Handle + eventHandler() + + case e := <-eventsCh: + // Prioritize exiting if context is canceled + if ctx.Err() != nil { + return + } + + // Try to re-establish the watcher for events which dropped the existing watch + if e.Name == path && (e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename)) { + _ = watcher.Remove(path) + if err := watcher.Add(path); err != nil { + errorHandler(err) + attemptPeriodicRewatch = true + } + } + + // Handle + eventHandler() + + case err := <-errorCh: + // Prioritize exiting if context is canceled + if ctx.Err() != nil { + return + } + + // If the error occurs in response to calling watcher.Add, re-adding here could hot-loop. + // The periodic poll will attempt to re-establish the watch. + errorHandler(err) + attemptPeriodicRewatch = true + } + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil_test.go new file mode 100644 index 000000000..fe53af672 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/filesystem/watchuntil_test.go @@ -0,0 +1,257 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesystem + +import ( + "context" + "os" + "path/filepath" + "sync/atomic" + "testing" + "time" +) + +func TestWatchUntilPanicsOnInvalidArgs(t *testing.T) { + t.Run("zero pollInterval panics", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic for zero pollInterval") + } + }() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + WatchUntil(ctx, 0, "/tmp", func() {}, nil) + }) + + t.Run("negative pollInterval panics", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic for negative pollInterval") + } + }() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + WatchUntil(ctx, -1*time.Second, "/tmp", func() {}, nil) + }) + + t.Run("nil eventHandler panics", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic for nil eventHandler") + } + }() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + WatchUntil(ctx, time.Second, "/tmp", nil, nil) + }) +} + +func TestWatchUntilContextCancellation(t *testing.T) { + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + var callCount atomic.Int32 + + done := make(chan struct{}) + go func() { + defer close(done) + WatchUntil(ctx, 50*time.Millisecond, dir, func() { + callCount.Add(1) + }, nil) + }() + + // Wait for at least one handler invocation + waitForCondition(t, 2*time.Second, func() bool { + return callCount.Load() > 0 + }) + + cancel() + + select { + case <-done: + // WatchUntil returned after context cancellation + case <-time.After(5 * time.Second): + t.Fatal("WatchUntil did not return after context cancellation") + } +} + +func TestWatchUntilCallsHandlerOnPoll(t *testing.T) { + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var callCount atomic.Int32 + + go func() { + WatchUntil(ctx, 50*time.Millisecond, dir, func() { + callCount.Add(1) + }, nil) + }() + + // Should get called multiple times via polling + waitForCondition(t, 2*time.Second, func() bool { + return callCount.Load() >= 3 + }) +} + +func TestWatchUntilCallsHandlerOnFileChange(t *testing.T) { + dir := t.TempDir() + testFile := filepath.Join(dir, "test.yaml") + if err := os.WriteFile(testFile, []byte("initial"), 0644); err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var callCount atomic.Int32 + + go func() { + WatchUntil(ctx, 10*time.Minute, dir, func() { + callCount.Add(1) + }, nil) + }() + + // Wait for initial handler call + waitForCondition(t, 2*time.Second, func() bool { + return callCount.Load() >= 1 + }) + + initialCount := callCount.Load() + + // Modify the file to trigger a filesystem event + if err := os.WriteFile(testFile, []byte("modified"), 0644); err != nil { + t.Fatal(err) + } + + // Should get called again due to file change + waitForCondition(t, 5*time.Second, func() bool { + return callCount.Load() > initialCount + }) +} + +func TestWatchUntilNonExistentPathCallsErrorHandler(t *testing.T) { + nonExistent := filepath.Join(t.TempDir(), "does-not-exist") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var errorCount atomic.Int32 + var eventCount atomic.Int32 + + go func() { + WatchUntil(ctx, 50*time.Millisecond, nonExistent, func() { + eventCount.Add(1) + }, func(err error) { + errorCount.Add(1) + }) + }() + + // Error handler should be called for the non-existent path + waitForCondition(t, 2*time.Second, func() bool { + return errorCount.Load() > 0 + }) + + // Event handler should still be called via polling + waitForCondition(t, 2*time.Second, func() bool { + return eventCount.Load() > 0 + }) +} + +func TestWatchUntilNilErrorHandler(t *testing.T) { + nonExistent := filepath.Join(t.TempDir(), "does-not-exist") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var eventCount atomic.Int32 + + // nil errorHandler should not panic + done := make(chan struct{}) + go func() { + defer close(done) + WatchUntil(ctx, 50*time.Millisecond, nonExistent, func() { + eventCount.Add(1) + }, nil) + }() + + // Should still work with polling + waitForCondition(t, 2*time.Second, func() bool { + return eventCount.Load() > 0 + }) + + cancel() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("WatchUntil did not return") + } +} + +func TestWatchUntilDetectsAtomicRename(t *testing.T) { + dir := t.TempDir() + testFile := filepath.Join(dir, "config.yaml") + if err := os.WriteFile(testFile, []byte("version: 1"), 0644); err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var callCount atomic.Int32 + + go func() { + // Use a long poll interval so we rely on fsnotify, not polling + WatchUntil(ctx, 10*time.Minute, dir, func() { + callCount.Add(1) + }, nil) + }() + + // Wait for initial handler call + waitForCondition(t, 2*time.Second, func() bool { + return callCount.Load() >= 1 + }) + + countBefore := callCount.Load() + + // Atomic file replacement: write temp, then rename (recommended KEP pattern) + tmpFile := filepath.Join(dir, ".config.yaml.tmp") + if err := os.WriteFile(tmpFile, []byte("version: 2"), 0644); err != nil { + t.Fatal(err) + } + if err := os.Rename(tmpFile, testFile); err != nil { + t.Fatal(err) + } + + // Should detect the rename event + waitForCondition(t, 5*time.Second, func() bool { + return callCount.Load() > countBefore + }) +} + +func waitForCondition(t *testing.T, timeout time.Duration, condition func() bool) { + t.Helper() + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if condition() { + return + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("condition not met within %v", timeout) +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_context.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_context.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_context.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_context.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_controller.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_controller.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_controller.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_controller.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_controller_debug.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_controller_debug.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_controller_debug.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_controller_debug.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_filter.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_filter.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_filter.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_filter.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_filter_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_filter_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/apf_filter_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/apf_filter_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/borrowing_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/borrowing_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/borrowing_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/borrowing_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/conc_alloc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/conc_alloc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/conc_alloc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/conc_alloc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/conc_alloc_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/conc_alloc_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/conc_alloc_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/conc_alloc_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/controller_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/controller_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/controller_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/controller_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/counter/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/counter/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/counter/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/counter/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/debug/dump.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/debug/dump.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/debug/dump.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/debug/dump.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/dropped_requests_tracker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/dropped_requests_tracker.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/dropped_requests_tracker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/dropped_requests_tracker.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/dropped_requests_tracker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/dropped_requests_tracker_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/dropped_requests_tracker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/dropped_requests_tracker_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/exempt_borrowing_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/exempt_borrowing_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/exempt_borrowing_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/exempt_borrowing_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/real.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/real.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/real.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/real.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/real_event_clock_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/real_event_clock_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/eventclock/real_event_clock_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/eventclock/real_event_clock_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/integrator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/integrator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/integrator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/integrator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/integrator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/integrator_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/integrator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/integrator_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/promise.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/promise.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/promise.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/promise.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/promise_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/promise_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/promise/promise_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/promise/promise_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/fifo_list_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/queueset_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/types.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/types.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/queueset/types.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/queueset/types.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake_event_clock_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake_event_clock_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake_event_clock_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/eventclock/fake_event_clock_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/no-restraint.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/no-restraint.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/no-restraint.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/no-restraint.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/fairqueuing/testing/promise/counting_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/filter_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/filter_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/filter_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/filter_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/format/formatting.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/format/formatting.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/format/formatting.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/format/formatting.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/formatting.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/formatting.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/formatting.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/formatting.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/gen_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/gen_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/gen_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/gen_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/match_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/match_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/match_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/match_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/max_seats.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/max_seats.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/max_seats.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/max_seats.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/max_seats_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/max_seats_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/max_seats_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/max_seats_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/interface.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/interface.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/interface.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/interface.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/timing_ratio_histogram_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/union_gauge.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/union_gauge.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/union_gauge.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/union_gauge.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/vec_element_pair.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/vec_element_pair.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/metrics/vec_element_pair.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/metrics/vec_element_pair.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/config.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/config.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/config.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/config.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/list_work_estimator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/list_work_estimator.go similarity index 95% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/list_work_estimator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/list_work_estimator.go index 8c84f20c8..c78a50cdd 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/list_work_estimator.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/list_work_estimator.go @@ -86,10 +86,12 @@ func (e *listWorkEstimator) estimate(r *http.Request, flowSchemaName, priorityLe return WorkEstimate{InitialSeats: maxSeats} } - // For watch requests, we want to adjust the cost only if they explicitly request - // sending initial events. + // For watch requests we want to set the cost low if they aren't requesting for init events, + // either via the explicit SendInitialEvents param or via legacy watches that have RV=0 or unset. if requestInfo.Verb == "watch" { - if listOptions.SendInitialEvents == nil || !*listOptions.SendInitialEvents { + sendInitEvents := listOptions.SendInitialEvents != nil && *listOptions.SendInitialEvents + legacyWatch := listOptions.ResourceVersion == "" || listOptions.ResourceVersion == "0" + if !sendInitEvents && !legacyWatch { return WorkEstimate{InitialSeats: e.config.MinimumSeats} } } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/list_work_estimator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/list_work_estimator_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/list_work_estimator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/list_work_estimator_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/mutating_work_estimator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/mutating_work_estimator.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/mutating_work_estimator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/mutating_work_estimator.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/object_count_tracker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/object_count_tracker.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/object_count_tracker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/object_count_tracker.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/object_count_tracker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/object_count_tracker_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/object_count_tracker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/object_count_tracker_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/seat_seconds.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/seat_seconds.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/seat_seconds.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/seat_seconds.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/seat_seconds_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/seat_seconds_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/seat_seconds_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/seat_seconds_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/width.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/width.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/width.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/width.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/width_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/width_test.go similarity index 89% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/width_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/width_test.go index 01dadab76..8c51aea80 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/request/width_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/request/width_test.go @@ -598,30 +598,79 @@ func TestWorkEstimator(t *testing.T) { initialSeatsExpected: 3, }, { - name: "request verb is watch, sendInitialEvents is nil", + name: "request verb is watch, sendInitialEvents is nil and RV unset (legacy watch with init events)", requestURI: "http://server/apis/foo.bar/v1/events?watch=true", requestInfo: &apirequest.RequestInfo{ Verb: "watch", APIGroup: "foo.bar", Resource: "events", }, - stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 10_000}, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, + { + name: "request verb is watch, sendInitialEvents is nil and RV set to 0 (legacy watch with init events)", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&resourceVersion=0", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, + { + name: "request verb is watch, sendInitialEvents is nil and RV set to non-zero (legacy watch without init events)", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&resourceVersion=1", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, initialSeatsExpected: 1, }, { - name: "request verb is watch, sendInitialEvents is false", + name: "request verb is watch, sendInitialEvents is false and RV unset (legacy watch with init events)", requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=false", requestInfo: &apirequest.RequestInfo{ Verb: "watch", APIGroup: "foo.bar", Resource: "events", }, - stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 10_000}, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, + { + name: "request verb is watch, sendInitialEvents is false and RV set to 0 (legacy watch with init events)", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=false&resourceVersion=0", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, + { + name: "request verb is watch, sendInitialEvents is false and RV set to non-zero (legacy watch without init events))", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=false&resourceVersion=1", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, maxSeats: 100, initialSeatsExpected: 1, }, { - name: "request verb is watch, sendInitialEvents is true", + name: "request verb is watch, sendInitialEvents is true and RV unset (streaming list with init events)", requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=true", requestInfo: &apirequest.RequestInfo{ Verb: "watch", @@ -632,6 +681,30 @@ func TestWorkEstimator(t *testing.T) { maxSeats: 100, initialSeatsExpected: 8, }, + { + name: "request verb is watch, sendInitialEvents is true and RV set to 0 (streaming list with init events)", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=true&resourceVersion=0", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, + { + name: "request verb is watch, sendInitialEvents is true and RV set to non-zero (streaming list with init events)", + requestURI: "http://server/apis/foo.bar/v1/events?watch=true&sendInitialEvents=true&resourceVersion=0", + requestInfo: &apirequest.RequestInfo{ + Verb: "watch", + APIGroup: "foo.bar", + Resource: "events", + }, + stats: storage.Stats{ObjectCount: 799, EstimatedAverageObjectSizeBytes: 1_000}, + maxSeats: 100, + initialSeatsExpected: 8, + }, { name: "request verb is create, no watches", requestURI: "http://server/apis/foo.bar/v1/foos", diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/rule.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/rule.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/rule.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/rule.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/watch_tracker.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/watch_tracker.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/watch_tracker.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/watch_tracker.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/watch_tracker_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/watch_tracker_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flowcontrol/watch_tracker_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flowcontrol/watch_tracker_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/writer.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/writer.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/writer.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/writer.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/writer_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/writer_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/flushwriter/writer_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/flushwriter/writer_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/notfoundhandler/not_found_handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/notfoundhandler/not_found_handler.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/notfoundhandler/not_found_handler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/notfoundhandler/not_found_handler.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/notfoundhandler/not_found_handler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/notfoundhandler/not_found_handler_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/notfoundhandler/not_found_handler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/notfoundhandler/not_found_handler_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/enablement.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/enablement.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/enablement.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/enablement.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/enablement_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/enablement_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/enablement_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/enablement_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/proto.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/proto.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/proto.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/proto.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/proto_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/proto_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/openapi/proto_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/openapi/proto_test.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager.go new file mode 100644 index 000000000..e7fb00710 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager.go @@ -0,0 +1,492 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package peerproxy + +import ( + "context" + "sync/atomic" + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog/v2" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" +) + +// GVExtractor is a function that extracts group-versions from an object. +// It returns a slice of GroupVersions belonging to CRDs or aggregated APIs that +// should be excluded from peer-discovery to avoid advertising stale CRDs/aggregated APIs +// in peer-aggregated discovery that were deleted but still appear in a peer's discovery. +type GVExtractor func(obj interface{}) []schema.GroupVersion + +// GVExclusionManager manages the exclusion of group-versions from peer discovery. +// It maintains two atomic maps: +// - currentlyActiveGVs: All GVs currently served by CRDs and aggregated APIServices +// - recentlyDeletedGVs: GVs belonging to CRDs and aggregated APIServices that were recently deleted, +// tracked with deletion timestamp for grace period +// +// It runs two workers: +// 1. Active GV Tracker: Triggered on CRD/APIService events, rebuilds active GVs +// and reaps expired deleted GVs. When a GV is deleted, a delayed sync is scheduled +// after the grace period to reap it. +// 2. Peer Discovery Re-filter: Rate-limited worker that filters peer cache +type GVExclusionManager struct { + // Atomic maps for lock-free access + currentlyActiveGVs atomic.Value // map[schema.GroupVersion]struct{} + recentlyDeletedGVs atomic.Value // map[schema.GroupVersion]time.Time + + // Informers for fetching active GVs + crdInformer cache.SharedIndexInformer + crdExtractor GVExtractor + apiServiceInformer cache.SharedIndexInformer + apiServiceExtractor GVExtractor + + // Worker 1: triggered by CRD/APIService events or delayed reap scheduling + activeGVQueue workqueue.TypedRateLimitingInterface[string] + // Grace period before reaping deleted GVs from the exclusion set + exclusionGracePeriod time.Duration + // Worker 2: triggered by Active/Deleted GV changes + refilterQueue workqueue.TypedRateLimitingInterface[string] + + // rawPeerDiscoveryCache is written only by peerLeaseQueue worker + // when peer leases change + rawPeerDiscoveryCache *atomic.Value // map[string]PeerDiscoveryCacheEntry + // filteredPeerDiscoveryCache is written only by the refilter worker + // when raw cache or exclusion set changes. + filteredPeerDiscoveryCache atomic.Value // map[string]PeerDiscoveryCacheEntry + invalidationCallback *atomic.Pointer[func()] +} + +// NewGVExclusionManager creates a new GV exclusion manager. +func NewGVExclusionManager( + exclusionGracePeriod time.Duration, + rawPeerDiscoveryCache *atomic.Value, + invalidationCallback *atomic.Pointer[func()], +) *GVExclusionManager { + mgr := &GVExclusionManager{ + exclusionGracePeriod: exclusionGracePeriod, + rawPeerDiscoveryCache: rawPeerDiscoveryCache, + invalidationCallback: invalidationCallback, + activeGVQueue: workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.DefaultTypedControllerRateLimiter[string](), + workqueue.TypedRateLimitingQueueConfig[string]{ + Name: "active-gv-tracker", + }), + refilterQueue: workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.DefaultTypedControllerRateLimiter[string](), + workqueue.TypedRateLimitingQueueConfig[string]{ + Name: "peer-discovery-refilter", + }), + } + + mgr.currentlyActiveGVs.Store(map[schema.GroupVersion]struct{}{}) + mgr.recentlyDeletedGVs.Store(map[schema.GroupVersion]time.Time{}) + mgr.filteredPeerDiscoveryCache.Store(map[string]PeerDiscoveryCacheEntry{}) + + return mgr +} + +// GetFilteredPeerDiscoveryCache returns the filtered peer discovery cache. +func (m *GVExclusionManager) GetFilteredPeerDiscoveryCache() map[string]PeerDiscoveryCacheEntry { + if cache := m.filteredPeerDiscoveryCache.Load(); cache != nil { + if cacheMap, ok := cache.(map[string]PeerDiscoveryCacheEntry); ok { + return cacheMap + } + } + return map[string]PeerDiscoveryCacheEntry{} +} + +// RegisterCRDInformerHandlers registers event handlers for CRD informer using a custom extractor. +// The extractor function is responsible for extracting GroupVersions from CRD objects. +func (m *GVExclusionManager) RegisterCRDInformerHandlers(crdInformer cache.SharedIndexInformer, extractor GVExtractor) error { + m.crdInformer = crdInformer + m.crdExtractor = extractor + _, err := m.crdInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + m.handleGVUpdate() + }, + UpdateFunc: func(oldObj, newObj interface{}) { + m.handleGVUpdate() + }, + DeleteFunc: func(obj interface{}) { + m.handleGVUpdate() + }, + }) + return err +} + +// RegisterAPIServiceInformerHandlers registers event handlers for APIService informer using a custom extractor. +// The extractor function is responsible for extracting GroupVersions from APIService objects +// and determining if they represent aggregated APIs. +func (m *GVExclusionManager) RegisterAPIServiceInformerHandlers(apiServiceInformer cache.SharedIndexInformer, extractor GVExtractor) error { + m.apiServiceInformer = apiServiceInformer + m.apiServiceExtractor = extractor + _, err := m.apiServiceInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + m.handleGVUpdate() + }, + UpdateFunc: func(oldObj, newObj interface{}) { + m.handleGVUpdate() + }, + DeleteFunc: func(obj interface{}) { + m.handleGVUpdate() + }, + }) + return err +} + +// WaitForCacheSync waits for the informer caches to sync. +func (m *GVExclusionManager) WaitForCacheSync(stopCh <-chan struct{}) bool { + synced := []cache.InformerSynced{} + if m.crdInformer != nil { + synced = append(synced, m.crdInformer.HasSynced) + } + if m.apiServiceInformer != nil { + synced = append(synced, m.apiServiceInformer.HasSynced) + } + if len(synced) == 0 { + return true + } + return cache.WaitForNamedCacheSync("gv-exclusion-manager", stopCh, synced...) +} + +// getExclusionSet returns the combined exclusion set i.e., union(currentlyActiveGVs, recentlyDeletedGVs) +func (m *GVExclusionManager) getExclusionSet() map[schema.GroupVersion]struct{} { + activeMap := m.loadCurrentlyActiveGVs() + deletedMap := m.loadRecentlyDeletedGVs() + + exclusionSet := make(map[schema.GroupVersion]struct{}, len(activeMap)+len(deletedMap)) + for gv := range activeMap { + exclusionSet[gv] = struct{}{} + } + for gv := range deletedMap { + exclusionSet[gv] = struct{}{} + } + + return exclusionSet +} + +// handleGVUpdate is called when a CRD or APIService event occurs. +// This triggers reconciliation which rebuilds the active GV set +// and also reaps expired GVs if indicated so. +func (m *GVExclusionManager) handleGVUpdate() { + m.activeGVQueue.Add("sync") +} + +// RunPeerDiscoveryActiveGVTracker runs the Active GV Tracker worker. +// This worker is triggered by CRD/APIService events and +// rebuilds the active GV set and reaps expired GVs. +func (m *GVExclusionManager) RunPeerDiscoveryActiveGVTracker(ctx context.Context) { + defer m.activeGVQueue.ShutDown() + + klog.Info("Starting Active GV Tracker worker") + go wait.UntilWithContext(ctx, m.runActiveGVTrackerWorker, time.Second) + + <-ctx.Done() + klog.Info("Active GV Tracker workers stopped") +} + +func (m *GVExclusionManager) runActiveGVTrackerWorker(ctx context.Context) { + for m.processNextActiveGV(ctx) { + } +} + +func (m *GVExclusionManager) processNextActiveGV(ctx context.Context) bool { + key, shutdown := m.activeGVQueue.Get() + if shutdown { + return false + } + defer m.activeGVQueue.Done(key) + + select { + case <-ctx.Done(): + return false + default: + } + + m.reconcileActiveGVs() + return true +} + +// reconcileActiveGVs does the following +// 1. fetches all GVs from CRD and APIService informers +// 2. detects diffs with the previous state +// 3. adds deleted GVs to recentlyDeletedGVs +// 4. reaps expired GVs, and queues Worker 2 if changes detected. +func (m *GVExclusionManager) reconcileActiveGVs() { + freshGVs := make(map[schema.GroupVersion]struct{}) + + // Fetch GVs from CRD informer + if m.crdInformer != nil && m.crdExtractor != nil { + crdList := m.crdInformer.GetStore().List() + for _, item := range crdList { + gvs := m.crdExtractor(item) + for _, gv := range gvs { + freshGVs[gv] = struct{}{} + } + } + } + + // Fetch GVs from APIService informer + if m.apiServiceInformer != nil && m.apiServiceExtractor != nil { + apiSvcList := m.apiServiceInformer.GetStore().List() + for _, item := range apiSvcList { + gvs := m.apiServiceExtractor(item) + for _, gv := range gvs { + freshGVs[gv] = struct{}{} + } + } + } + + // Load previous active GVs and detect diff + previousGVs := m.loadCurrentlyActiveGVs() + deletedGVs, activeGVsChanged := diffGVs(previousGVs, freshGVs) + + // Update recentlyDeletedGVs: add newly deleted GVs and reap expired ones + recentlyDeletedChanged := m.updateRecentlyDeletedGVs(deletedGVs) + + if activeGVsChanged || recentlyDeletedChanged { + if activeGVsChanged { + m.currentlyActiveGVs.Store(freshGVs) + klog.V(4).Infof("Active GVs updated: %d GVs now active", len(freshGVs)) + } + m.TriggerRefilter() + } else { + klog.V(4).Infof("No changes detected in active or recently deleted GVs") + } +} + +// updateRecentlyDeletedGVs adds newly deleted GVs to recentlyDeletedGVs +// and reaps expired ones. Returns true if any changes were made. +func (m *GVExclusionManager) updateRecentlyDeletedGVs(deletedGVs []schema.GroupVersion) bool { + deletedMap := m.loadRecentlyDeletedGVs() + // Early return if nothing to do + if len(deletedGVs) == 0 && len(deletedMap) == 0 { + return false + } + + now := time.Now() + newDeletedMap := make(map[schema.GroupVersion]time.Time, len(deletedMap)+len(deletedGVs)) + changed := false + + // Copy existing entries, reaping expired ones + for gv, deletionTime := range deletedMap { + if now.Sub(deletionTime) > m.exclusionGracePeriod { + klog.V(4).Infof("Reaping GV %s (grace period expired)", gv.String()) + changed = true + // Don't add to new map (effectively removing it) + } else { + newDeletedMap[gv] = deletionTime + } + } + + // Schedule a delayed sync to reap expired GVs after the grace period. + if len(deletedGVs) > 0 { + m.activeGVQueue.AddAfter("sync", m.exclusionGracePeriod) + } + + for _, gv := range deletedGVs { + newDeletedMap[gv] = now + klog.V(4).Infof("GV %s deleted: moved to recentlyDeletedGVs", gv.String()) + changed = true + } + + if changed { + m.recentlyDeletedGVs.Store(newDeletedMap) + } + + return changed +} + +// diffGVs compares old and new GV maps, returns GVs that were deleted (in old but not new) +// and a boolean indicating if there were any changes (additions or deletions). +func diffGVs(old, new map[schema.GroupVersion]struct{}) ([]schema.GroupVersion, bool) { + var deletedGVs []schema.GroupVersion + hasChanges := len(old) != len(new) + + // Find deleted GVs (in old but not in new) + for gv := range old { + if _, exists := new[gv]; !exists { + deletedGVs = append(deletedGVs, gv) + hasChanges = true + } + } + + return deletedGVs, hasChanges +} + +// RunPeerDiscoveryRefilter runs the Peer Discovery Re-filter worker. +// This worker filters the peer discovery cache using the exclusion set. +func (m *GVExclusionManager) RunPeerDiscoveryRefilter(ctx context.Context) { + defer m.refilterQueue.ShutDown() + + klog.Info("Starting Peer Discovery Re-filter worker") + go wait.UntilWithContext(ctx, m.runRefilterWorker, time.Second) + + <-ctx.Done() + klog.Info("Peer Discovery Re-filter workers stopped") +} + +func (m *GVExclusionManager) runRefilterWorker(ctx context.Context) { + for m.processNextRefilter(ctx) { + } +} + +func (m *GVExclusionManager) processNextRefilter(ctx context.Context) bool { + key, shutdown := m.refilterQueue.Get() + if shutdown { + return false + } + defer m.refilterQueue.Done(key) + + select { + case <-ctx.Done(): + return false + default: + } + + m.refilterPeerDiscoveryCache() + return true +} + +// refilterPeerDiscoveryCache reads the raw peer discovery cache, +// applies exclusion filtering, and stores the result to the filtered cache. +func (m *GVExclusionManager) refilterPeerDiscoveryCache() { + var cacheMap map[string]PeerDiscoveryCacheEntry + if m.rawPeerDiscoveryCache != nil { + if rawCache := m.rawPeerDiscoveryCache.Load(); rawCache != nil { + cacheMap, _ = rawCache.(map[string]PeerDiscoveryCacheEntry) + } + } + + if len(cacheMap) == 0 { + m.filteredPeerDiscoveryCache.Store(map[string]PeerDiscoveryCacheEntry{}) + klog.V(4).Infof("Raw peer discovery cache is empty or unavailable") + return + } + + filteredCache := m.filterPeerDiscoveryCache(cacheMap) + m.filteredPeerDiscoveryCache.Store(filteredCache) + + if m.invalidationCallback != nil { + if callback := m.invalidationCallback.Load(); callback != nil { + (*callback)() + } + } + + klog.V(4).Infof("Peer discovery cache re-filtered, %d GVs in exclusion set", len(m.getExclusionSet())) + +} + +// filterPeerDiscoveryCache applies the current exclusion set to the provided peer cache. +func (m *GVExclusionManager) filterPeerDiscoveryCache(cacheMap map[string]PeerDiscoveryCacheEntry) map[string]PeerDiscoveryCacheEntry { + exclusionSet := m.getExclusionSet() + if len(exclusionSet) == 0 { + return cacheMap + } + + filtered := make(map[string]PeerDiscoveryCacheEntry, len(cacheMap)) + for peerID, entry := range cacheMap { + filtered[peerID] = m.filterPeerCacheEntry(entry, exclusionSet) + } + + return filtered +} + +// filterPeerCacheEntry filters a single peer's cache entry for excluded GVs. +func (m *GVExclusionManager) filterPeerCacheEntry( + entry PeerDiscoveryCacheEntry, + exclusionSet map[schema.GroupVersion]struct{}, +) PeerDiscoveryCacheEntry { + filteredGVRs := make(map[schema.GroupVersionResource]bool, len(entry.GVRs)) + anyExcluded := false + for existingGVR, v := range entry.GVRs { + gv := schema.GroupVersion{Group: existingGVR.Group, Version: existingGVR.Version} + if _, excluded := exclusionSet[gv]; excluded { + anyExcluded = true + continue + } + filteredGVRs[existingGVR] = v + } + + // If no GVRs were excluded, the exclusion set doesn't intersect with this peer's GVs, + // so we can return the entry unchanged without filtering GroupDiscovery. + if !anyExcluded { + return entry + } + filteredGroups := m.filterGroupDiscovery(entry.GroupDiscovery, exclusionSet) + + return PeerDiscoveryCacheEntry{ + GVRs: filteredGVRs, + GroupDiscovery: filteredGroups, + } +} + +// filterGroupDiscovery filters group discovery entries, removing excluded GVs. +func (m *GVExclusionManager) filterGroupDiscovery( + groupDiscoveries []apidiscoveryv2.APIGroupDiscovery, + exclusionSet map[schema.GroupVersion]struct{}, +) []apidiscoveryv2.APIGroupDiscovery { + var filteredDiscovery []apidiscoveryv2.APIGroupDiscovery + for _, groupDiscovery := range groupDiscoveries { + filteredGroup := apidiscoveryv2.APIGroupDiscovery{ + ObjectMeta: groupDiscovery.ObjectMeta, + } + + for _, version := range groupDiscovery.Versions { + gv := schema.GroupVersion{Group: groupDiscovery.Name, Version: version.Version} + if _, found := exclusionSet[gv]; found { + // This version is excluded, skip it + continue + } + filteredGroup.Versions = append(filteredGroup.Versions, version) + } + + // Only add the group to the final list if it still has any versions left + if len(filteredGroup.Versions) > 0 { + filteredDiscovery = append(filteredDiscovery, filteredGroup) + } + } + return filteredDiscovery +} + +func (m *GVExclusionManager) loadCurrentlyActiveGVs() map[schema.GroupVersion]struct{} { + if val := m.currentlyActiveGVs.Load(); val != nil { + if gvMap, ok := val.(map[schema.GroupVersion]struct{}); ok { + return gvMap + } + } + return map[schema.GroupVersion]struct{}{} +} + +func (m *GVExclusionManager) loadRecentlyDeletedGVs() map[schema.GroupVersion]time.Time { + if val := m.recentlyDeletedGVs.Load(); val != nil { + if gvMap, ok := val.(map[schema.GroupVersion]time.Time); ok { + return gvMap + } + } + return map[schema.GroupVersion]time.Time{} +} + +// TriggerRefilter triggers the refilter worker to apply exclusions to the filtered cache. +// This should be called by peerLeaseQueue after updating the raw peer discovery cache. +func (m *GVExclusionManager) TriggerRefilter() { + m.refilterQueue.Add("refilter") +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager_test.go new file mode 100644 index 000000000..bc8125bf2 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/gv_exclusion_manager_test.go @@ -0,0 +1,589 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package peerproxy + +import ( + "slices" + "sync/atomic" + "testing" + "time" + + "k8s.io/apimachinery/pkg/runtime/schema" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetExclusionSet(t *testing.T) { + tests := []struct { + name string + activeGVs map[schema.GroupVersion]struct{} + deletedGVs map[schema.GroupVersion]time.Time + wantGVs []schema.GroupVersion + }{ + { + name: "empty state", + activeGVs: nil, + deletedGVs: nil, + wantGVs: []schema.GroupVersion{}, + }, + { + name: "only active GVs", + activeGVs: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + {Group: "batch", Version: "v1"}: {}, + {Group: "custom", Version: "v1alpha1"}: {}, + }, + wantGVs: []schema.GroupVersion{ + {Group: "apps", Version: "v1"}, + {Group: "batch", Version: "v1"}, + {Group: "custom", Version: "v1alpha1"}, + }, + }, + { + name: "only deleted GVs", + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "deprecated", Version: "v1beta1"}: time.Now(), + {Group: "legacy", Version: "v1alpha1"}: time.Now(), + }, + // Reaper hasnt removed deleted GVs yet. + wantGVs: []schema.GroupVersion{ + {Group: "deprecated", Version: "v1beta1"}, + {Group: "legacy", Version: "v1alpha1"}, + }, + }, + { + name: "different active and deleted GVs", + activeGVs: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + {Group: "batch", Version: "v1"}: {}, + }, + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "old", Version: "v1"}: time.Now(), + }, + // Include both active GVs and recently deleted GVs. + // Deleted GVs remain in the exclusion set until the reaper removes them after the grace period. + wantGVs: []schema.GroupVersion{ + {Group: "apps", Version: "v1"}, + {Group: "batch", Version: "v1"}, + {Group: "old", Version: "v1"}, + }, + }, + { + // A GV can appear in both active and deleted sets if: + // 1. CRD was deleted (moved from active to deleted) + // 2. CRD was recreated (added back to active) + // 3. Reaper hasn't cleaned up the deleted entry yet + name: "same GV in both active and deleted", + activeGVs: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "apps", Version: "v1"}: time.Now(), + }, + wantGVs: []schema.GroupVersion{ + {Group: "apps", Version: "v1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mgr := NewGVExclusionManager(5*time.Minute, &atomic.Value{}, &atomic.Pointer[func()]{}) + + if tt.activeGVs != nil { + mgr.currentlyActiveGVs.Store(tt.activeGVs) + } + if tt.deletedGVs != nil { + mgr.recentlyDeletedGVs.Store(tt.deletedGVs) + } + + exclusionSet := mgr.getExclusionSet() + if len(exclusionSet) != len(tt.wantGVs) { + t.Errorf("Want exclusion set size %d, got %d", len(tt.wantGVs), len(exclusionSet)) + } + + for _, gv := range tt.wantGVs { + if _, found := exclusionSet[gv]; !found { + t.Errorf("Want GV %v in exclusion set, but not found", gv) + } + } + }) + } +} + +func TestReapExpiredGVs(t *testing.T) { + tests := []struct { + name string + gracePeriod time.Duration + deletedGVs map[schema.GroupVersion]time.Time + wantReaped []schema.GroupVersion + }{ + { + name: "empty state", + gracePeriod: 100 * time.Millisecond, + deletedGVs: map[schema.GroupVersion]time.Time{}, + wantReaped: []schema.GroupVersion{}, + }, + { + name: "reap old GV keep recent", + gracePeriod: 100 * time.Millisecond, + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "old", Version: "v1"}: time.Now().Add(-200 * time.Millisecond), + {Group: "recent", Version: "v1"}: time.Now().Add(-50 * time.Millisecond), + {Group: "new", Version: "v1"}: time.Now(), + }, + wantReaped: []schema.GroupVersion{ + {Group: "old", Version: "v1"}, + }, + }, + { + name: "all expired", + gracePeriod: 50 * time.Millisecond, + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "old1", Version: "v1"}: time.Now().Add(-100 * time.Millisecond), + {Group: "old2", Version: "v1"}: time.Now().Add(-200 * time.Millisecond), + }, + wantReaped: []schema.GroupVersion{ + {Group: "old1", Version: "v1"}, + {Group: "old2", Version: "v1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mgr := NewGVExclusionManager(tt.gracePeriod, &atomic.Value{}, &atomic.Pointer[func()]{}) + mgr.recentlyDeletedGVs.Store(tt.deletedGVs) + mgr.updateRecentlyDeletedGVs(nil) + result := mgr.loadRecentlyDeletedGVs() + + for _, gv := range tt.wantReaped { + if _, found := result[gv]; found { + t.Errorf("GV %v should have been reaped but still exists", gv) + } + } + }) + } +} + +func TestDetectDiff(t *testing.T) { + _ = NewGVExclusionManager(5*time.Minute, &atomic.Value{}, &atomic.Pointer[func()]{}) + + tests := []struct { + name string + old map[schema.GroupVersion]struct{} + new map[schema.GroupVersion]struct{} + wantChanged bool + wantDeleted []schema.GroupVersion + }{ + { + name: "both empty", + old: map[schema.GroupVersion]struct{}{}, + new: map[schema.GroupVersion]struct{}{}, + wantChanged: false, + wantDeleted: nil, + }, + { + name: "identical", + old: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + new: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantChanged: false, + wantDeleted: nil, + }, + { + name: "added GV", + old: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + new: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + {Group: "batch", Version: "v1"}: {}, + }, + wantChanged: true, + wantDeleted: nil, // No deletions, only addition + }, + { + name: "removed GV", + old: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + {Group: "batch", Version: "v1"}: {}, + }, + new: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantChanged: true, + wantDeleted: []schema.GroupVersion{ + {Group: "batch", Version: "v1"}, + }, + }, + { + name: "different GVs same size", + old: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + new: map[schema.GroupVersion]struct{}{ + {Group: "batch", Version: "v1"}: {}, + }, + wantChanged: true, + wantDeleted: []schema.GroupVersion{ + {Group: "apps", Version: "v1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deletedGVs, changed := diffGVs(tt.old, tt.new) + if changed != tt.wantChanged { + t.Errorf("diffGVs() changed = %v, want %v", changed, tt.wantChanged) + } + if len(deletedGVs) != len(tt.wantDeleted) { + t.Errorf("diffGVs() deleted count = %d, want %d", len(deletedGVs), len(tt.wantDeleted)) + } + for _, wantGV := range tt.wantDeleted { + if !slices.Contains(deletedGVs, wantGV) { + t.Errorf("diffGVs() deleted missing GV %v", wantGV) + } + } + }) + } +} + +func TestFilterPeerDiscoveryCache(t *testing.T) { + tests := []struct { + name string + activeGVs map[schema.GroupVersion]struct{} + deletedGVs map[schema.GroupVersion]time.Time + cacheMap map[string]PeerDiscoveryCacheEntry + wantPeerGVRs map[string]int // peer name -> GVR count + }{ + { + name: "empty exclusion no changes", + activeGVs: nil, + cacheMap: map[string]PeerDiscoveryCacheEntry{ + "peer1": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + }, + }, + "peer2": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + }, + }, + }, + wantPeerGVRs: map[string]int{ + "peer1": 1, + "peer2": 1, + }, + }, + { + name: "filter active GVs", + activeGVs: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + cacheMap: map[string]PeerDiscoveryCacheEntry{ + "peer1": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + }, + }, + "peer2": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + }, + }, + }, + wantPeerGVRs: map[string]int{ + "peer1": 0, // apps/v1 filtered out + "peer2": 1, // unchanged + }, + }, + { + // Recently deleted GVs are still filtered from peer discovery during the + // grace period (before the reaper cleans them up) to avoid routing requests + // to peers for GVs that were just deleted locally. + name: "filter deleted GVs", + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "custom", Version: "v1alpha1"}: time.Now(), + }, + cacheMap: map[string]PeerDiscoveryCacheEntry{ + "peer1": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "custom", Version: "v1alpha1", Resource: "myresources"}: true, + }, + }, + "peer2": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + }, + }, + }, + wantPeerGVRs: map[string]int{ + "peer1": 0, // custom/v1alpha1 filtered out + "peer2": 1, // unchanged + }, + }, + { + // Both active GVs and recently deleted GVs (within grace period, not yet + // cleaned up by the reaper) are filtered from peer discovery. + name: "filter both active and deleted GVs", + activeGVs: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + deletedGVs: map[schema.GroupVersion]time.Time{ + {Group: "custom", Version: "v1alpha1"}: time.Now(), + }, + cacheMap: map[string]PeerDiscoveryCacheEntry{ + "peer1": { + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + {Group: "custom", Version: "v1alpha1", Resource: "myresources"}: true, + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + }, + }, + }, + wantPeerGVRs: map[string]int{ + "peer1": 1, // apps/v1 and custom/v1alpha1 filtered out, batch/v1 remains + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mgr := NewGVExclusionManager(5*time.Minute, &atomic.Value{}, &atomic.Pointer[func()]{}) + + if tt.activeGVs != nil { + mgr.currentlyActiveGVs.Store(tt.activeGVs) + } + if tt.deletedGVs != nil { + mgr.recentlyDeletedGVs.Store(tt.deletedGVs) + } + + filtered := mgr.filterPeerDiscoveryCache(tt.cacheMap) + + if len(filtered) != len(tt.cacheMap) { + t.Errorf("Want %d peers in filtered cache, got %d", len(tt.cacheMap), len(filtered)) + } + + for peerName, wantCount := range tt.wantPeerGVRs { + entry, found := filtered[peerName] + if !found { + t.Errorf("Peer %s not found in filtered cache", peerName) + continue + } + if len(entry.GVRs) != wantCount { + t.Errorf("Peer %s: want %d GVRs, got %d", peerName, wantCount, len(entry.GVRs)) + } + } + }) + } +} + +func TestFilterPeerCacheEntry(t *testing.T) { + tests := []struct { + name string + entry PeerDiscoveryCacheEntry + exclusionSet map[schema.GroupVersion]struct{} + wantGVRs []schema.GroupVersionResource + }{ + { + name: "empty exclusion set returns entry unchanged", + entry: PeerDiscoveryCacheEntry{ + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + }, + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + }, + }, + exclusionSet: map[schema.GroupVersion]struct{}{}, + wantGVRs: []schema.GroupVersionResource{ + {Group: "apps", Version: "v1", Resource: "deployments"}, + {Group: "batch", Version: "v1", Resource: "jobs"}, + }, + }, + { + name: "filter GVRs and groups", + entry: PeerDiscoveryCacheEntry{ + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "apps", Version: "v1", Resource: "deployments"}: true, + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + {Group: "custom", Version: "v1", Resource: "myresources"}: true, + }, + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "batch"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + }, + }, + exclusionSet: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantGVRs: []schema.GroupVersionResource{ + {Group: "batch", Version: "v1", Resource: "jobs"}, + {Group: "custom", Version: "v1", Resource: "myresources"}, + }, + }, + { + name: "no changes when exclusion doesn't match", + entry: PeerDiscoveryCacheEntry{ + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "batch", Version: "v1", Resource: "jobs"}: true, + }, + }, + exclusionSet: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantGVRs: []schema.GroupVersionResource{ + {Group: "batch", Version: "v1", Resource: "jobs"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mgr := NewGVExclusionManager(5*time.Minute, &atomic.Value{}, &atomic.Pointer[func()]{}) + + filtered := mgr.filterPeerCacheEntry(tt.entry, tt.exclusionSet) + + for _, gvr := range tt.wantGVRs { + if _, found := filtered.GVRs[gvr]; !found { + t.Errorf("Want GVR %v to be present", gvr) + } + } + }) + } +} + +func TestFilterGroupDiscovery(t *testing.T) { + tests := []struct { + name string + groupDiscoveries []apidiscoveryv2.APIGroupDiscovery + exclusionSet map[schema.GroupVersion]struct{} + wantGroups map[string][]string // group name -> list of versions + }{ + { + name: "partial version exclusion", + groupDiscoveries: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + {Version: "v1beta1"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "batch"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + }, + exclusionSet: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantGroups: map[string][]string{ + "apps": {"v1beta1"}, + "batch": {"v1"}, + }, + }, + { + name: "all versions excluded", + groupDiscoveries: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "batch"}, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + {Version: "v1"}, + }, + }, + }, + exclusionSet: map[schema.GroupVersion]struct{}{ + {Group: "apps", Version: "v1"}: {}, + }, + wantGroups: map[string][]string{ + "batch": {"v1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mgr := NewGVExclusionManager(5*time.Minute, &atomic.Value{}, &atomic.Pointer[func()]{}) + + filtered := mgr.filterGroupDiscovery(tt.groupDiscoveries, tt.exclusionSet) + for groupName, wantVersionStrs := range tt.wantGroups { + var group *apidiscoveryv2.APIGroupDiscovery + for i := range filtered { + if filtered[i].Name == groupName { + group = &filtered[i] + break + } + } + + if group == nil { + t.Errorf("Want group %s not found in filtered results", groupName) + continue + } + + if len(group.Versions) != len(wantVersionStrs) { + t.Errorf("Group %s: want %d versions, got %d", groupName, len(wantVersionStrs), len(group.Versions)) + continue + } + + for _, wantVer := range wantVersionStrs { + found := false + for _, ver := range group.Versions { + if ver.Version == wantVer { + found = true + break + } + } + if !found { + t.Errorf("Group %s: want version %s not found", groupName, wantVer) + } + } + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery.go new file mode 100644 index 000000000..51ca2c6d1 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery.go @@ -0,0 +1,100 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package peerproxy + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/klog/v2" +) + +// RunLocalDiscoveryCacheSync populated the localDiscoveryInfoCache and +// starts a goroutine to periodically refresh the local discovery cache. +func (h *peerProxyHandler) RunLocalDiscoveryCacheSync(stopCh <-chan struct{}) error { + klog.Info("localDiscoveryCacheInvalidation goroutine started") + // Populate the cache initially. + if err := h.populateLocalDiscoveryCache(); err != nil { + return fmt.Errorf("failed to populate initial local discovery cache: %w", err) + } + + go func() { + for { + select { + case <-h.localDiscoveryCacheTicker.C: + klog.V(4).Infof("Invalidating local discovery cache") + if err := h.populateLocalDiscoveryCache(); err != nil { + klog.Errorf("Failed to repopulate local discovery cache: %v", err) + } + case <-stopCh: + klog.Info("localDiscoveryCacheInvalidation goroutine received stop signal") + if h.localDiscoveryCacheTicker != nil { + h.localDiscoveryCacheTicker.Stop() + klog.Info("localDiscoveryCacheTicker stopped") + } + klog.Info("localDiscoveryCacheInvalidation goroutine exiting") + return + } + } + }() + return nil +} + +func (h *peerProxyHandler) populateLocalDiscoveryCache() error { + _, resourcesByGV, _, err := h.discoveryClient.GroupsAndMaybeResources() + if err != nil { + return fmt.Errorf("error getting API group resources from discovery: %w", err) + } + + freshLocalDiscoveryResponse := map[schema.GroupVersionResource]bool{} + for gv, resources := range resourcesByGV { + for _, resource := range resources.APIResources { + gvr := gv.WithResource(resource.Name) + freshLocalDiscoveryResponse[gvr] = true + } + } + + h.localDiscoveryInfoCache.Store(freshLocalDiscoveryResponse) + // Signal that the cache has been populated. + h.localDiscoveryInfoCachePopulatedOnce.Do(func() { + close(h.localDiscoveryInfoCachePopulated) + }) + return nil +} + +// shouldServeLocally checks if the requested resource is present in the local +// discovery cache indicating the request can be served by this server. +func (h *peerProxyHandler) shouldServeLocally(gvr schema.GroupVersionResource) bool { + cacheValue := h.localDiscoveryInfoCache.Load() + if cacheValue == nil { + return false + } + + cache, ok := cacheValue.(map[schema.GroupVersionResource]bool) + if !ok { + klog.Warning("Invalid cache type in localDiscoveryInfoCache") + return false + } + + exists, ok := cache[gvr] + if !ok { + klog.V(4).Infof("resource not found for %v in local discovery cache\n", gvr.GroupVersion()) + return false + } + + return exists +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery_test.go new file mode 100644 index 000000000..273b75778 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/local_discovery_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package peerproxy + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestShouldServeLocally_Table(t *testing.T) { + testCases := []struct { + name string + cache map[schema.GroupVersionResource]bool + gvr schema.GroupVersionResource + want bool + }{ + { + name: "resource present and true", + cache: map[schema.GroupVersionResource]bool{{Group: "foo", Version: "v1", Resource: "bars"}: true}, + gvr: schema.GroupVersionResource{Group: "foo", Version: "v1", Resource: "bars"}, + want: true, + }, + { + name: "resource present and false", + cache: map[schema.GroupVersionResource]bool{{Group: "foo", Version: "v1", Resource: "bars"}: false}, + gvr: schema.GroupVersionResource{Group: "foo", Version: "v1", Resource: "bars"}, + want: false, + }, + { + name: "resource not present", + cache: map[schema.GroupVersionResource]bool{{Group: "foo", Version: "v1", Resource: "bars"}: true}, + gvr: schema.GroupVersionResource{Group: "foo", Version: "v1", Resource: "baz"}, + want: false, + }, + { + name: "empty cache", + cache: map[schema.GroupVersionResource]bool{}, + gvr: schema.GroupVersionResource{Group: "foo", Version: "v1", Resource: "bars"}, + want: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + h := &peerProxyHandler{} + h.localDiscoveryInfoCache.Store(tc.cache) + got := h.shouldServeLocally(tc.gvr) + if got != tc.want { + t.Errorf("shouldServeLocally(%v) = %v, want %v", tc.gvr, got, tc.want) + } + }) + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/metrics/metrics.go new file mode 100644 index 000000000..9c7c5e041 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/metrics/metrics.go @@ -0,0 +1,111 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "context" + "sync" + + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" +) + +const ( + subsystem = "apiserver" + statuscode = "code" + group = "group" + version = "version" + resource = "resource" + errorType = "type" + + // ProxyErrorEndpointResolution indicates a failure to resolve the network address of a peer apiserver. + ProxyErrorEndpointResolution = "endpoint_resolution" + // ProxyErrorTransport indicates a failure to build the proxy transport for the request. + ProxyErrorTransport = "proxy_transport" + + // DiscoveryErrorLeaseList indicates a failure to list apiserver identity leases. + DiscoveryErrorLeaseList = "lease_list" + // DiscoveryErrorHostPortResolution indicates a failure to resolve host/port from an identity lease. + DiscoveryErrorHostPortResolution = "hostport_resolution" + // DiscoveryErrorFetch indicates a failure to fetch discovery document from a peer. + DiscoveryErrorFetch = "fetch_discovery" +) + +var registerMetricsOnce sync.Once + +var ( + // peerProxiedRequestsTotal counts the number of requests that were proxied to a peer kube-apiserver. + peerProxiedRequestsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Subsystem: subsystem, + Name: "rerouted_request_total", + Help: `Total number of requests that were proxied to a peer kube-apiserver because the local apiserver was not capable of serving it, broken down by 'group', 'version', and 'resource' indicating the GVR of the request. If all three are empty (""), the request is a discovery request.`, + StabilityLevel: metrics.ALPHA, + }, + []string{statuscode, group, version, resource}, + ) + + // peerProxyErrorsTotal counts the number of errors encountered while proxying requests to a peer kube-apiserver. + peerProxyErrorsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Subsystem: subsystem, + Name: "peer_proxy_errors_total", + Help: "Total number of errors encountered while proxying requests to a peer kube apiserver", + StabilityLevel: metrics.ALPHA, + }, + []string{errorType, group, version, resource}, + ) + + // peerDiscoverySyncErrorsTotal counts the number of errors encountered while syncing discovery information from a peer kube-apiserver. + peerDiscoverySyncErrorsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Subsystem: subsystem, + Name: "peer_discovery_sync_errors_total", + Help: "Total number of errors encountered while syncing discovery information from a peer kube-apiserver", + StabilityLevel: metrics.ALPHA, + }, + []string{errorType}, + ) +) + +func Register() { + registerMetricsOnce.Do(func() { + legacyregistry.MustRegister(peerProxiedRequestsTotal) + legacyregistry.MustRegister(peerProxyErrorsTotal) + legacyregistry.MustRegister(peerDiscoverySyncErrorsTotal) + }) +} + +// Only used for tests. +func Reset() { + legacyregistry.Reset() +} + +// IncPeerProxiedRequest increments the # of proxied requests to peer kube-apiserver +func IncPeerProxiedRequest(ctx context.Context, status, g, v, r string) { + peerProxiedRequestsTotal.WithContext(ctx).WithLabelValues(status, g, v, r).Add(1) +} + +// IncPeerProxyError increments the # of errors encountered during peer proxying +func IncPeerProxyError(ctx context.Context, e, g, v, r string) { + peerProxyErrorsTotal.WithContext(ctx).WithLabelValues(e, g, v, r).Add(1) +} + +// IncPeerDiscoverySyncError increments the # of errors encountered during peer discovery sync +func IncPeerDiscoverySyncError(ctx context.Context, e string) { + peerDiscoverySyncErrorsTotal.WithContext(ctx).WithLabelValues(e).Add(1) +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery.go new file mode 100644 index 000000000..693d5faca --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery.go @@ -0,0 +1,297 @@ +/* +Copyright 2023 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 peerproxy + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/authentication/user" + peerproxymetrics "k8s.io/apiserver/pkg/util/peerproxy/metrics" + "k8s.io/client-go/discovery" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + v1 "k8s.io/api/coordination/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + apirequest "k8s.io/apiserver/pkg/endpoints/request" + responsewriterutil "k8s.io/apiserver/pkg/util/responsewriter" +) + +const ( + peerDiscoveryControllerName = "peer-discovery-cache-sync" + // maxRetries is the maximum number of retry attempts per lease, set to 20 to handle + // the race condition during API server startup where identity leases are created before + // endpoint leases. During initialization, peer discovery sync may attempt to fetch discovery + // from a peer before that peer has created its endpoint lease, resulting in "missing port in + // address" errors. With the default rate limiting (exponential backoff starting at 5ms), + // 20 retries per lease provides approximately 60-90 seconds of retry window, which is + // sufficient for both identity and endpoint leases to be established during normal startup. + maxRetries = 20 +) + +func (h *peerProxyHandler) RunPeerDiscoveryCacheSync(ctx context.Context, workers int) { + defer utilruntime.HandleCrashWithContext(ctx) + defer h.peerLeaseQueue.ShutDown() + defer func() { + err := h.apiserverIdentityInformer.Informer().RemoveEventHandler(h.leaseRegistration) + if err != nil { + klog.Warning("error removing leaseInformer eventhandler") + } + }() + + klog.Infof("Workers: %d", workers) + for i := 0; i < workers; i++ { + klog.Infof("Starting worker") + go wait.UntilWithContext(ctx, h.runWorker, time.Second) + } + <-ctx.Done() +} + +func (h *peerProxyHandler) enqueueLease(lease *v1.Lease) { + h.peerLeaseQueue.Add(lease.Name) +} + +func (h *peerProxyHandler) runWorker(ctx context.Context) { + for h.processNextElectionItem(ctx) { + } +} + +func (h *peerProxyHandler) processNextElectionItem(ctx context.Context) bool { + key, shutdown := h.peerLeaseQueue.Get() + if shutdown { + return false + } + defer h.peerLeaseQueue.Done(key) + + err := h.syncPeerDiscoveryCache(ctx) + h.handleSyncPeerDiscoveryCacheErr(err, key) + return true +} + +func (h *peerProxyHandler) syncPeerDiscoveryCache(ctx context.Context) error { + var fetchDiscoveryErr error + // Rebuild the peer discovery cache from available leases. + leases, err := h.apiserverIdentityInformer.Lister().List(h.identityLeaseLabelSelector) + if err != nil { + peerproxymetrics.IncPeerDiscoverySyncError(ctx, peerproxymetrics.DiscoveryErrorLeaseList) + utilruntime.HandleError(err) + return err + } + + newCache := map[string]PeerDiscoveryCacheEntry{} + for _, l := range leases { + _, ok := h.isValidPeerIdentityLease(l) + if !ok { + continue + } + + discoveryEntry, err := h.fetchNewDiscoveryFor(ctx, l.Name) + if err != nil { + fetchDiscoveryErr = err + } + // Only add if there is at least one GVR or group + if len(discoveryEntry.GVRs) > 0 || len(discoveryEntry.GroupDiscovery) > 0 { + newCache[l.Name] = discoveryEntry + } + } + + // Store unfiltered data to raw cache and trigger refilter. + // The refilter worker (single writer to filtered cache) will apply exclusions. + h.rawPeerDiscoveryCache.Store(newCache) + h.gvExclusionManager.TriggerRefilter() + return fetchDiscoveryErr +} + +func (h *peerProxyHandler) fetchNewDiscoveryFor(ctx context.Context, serverID string) (PeerDiscoveryCacheEntry, error) { + hostport, err := h.hostportInfo(serverID) + if err != nil { + peerproxymetrics.IncPeerDiscoverySyncError(ctx, peerproxymetrics.DiscoveryErrorHostPortResolution) + return PeerDiscoveryCacheEntry{}, fmt.Errorf("failed to get host port info from identity lease for server %s: %w", serverID, err) + } + + klog.V(4).Infof("Proxying an agg-discovery call from %s to %s", h.serverID, serverID) + gvrMap := make(map[schema.GroupVersionResource]bool) + var discoveryErr error + var discoveryResponse *apidiscoveryv2.APIGroupDiscoveryList + discoveryPaths := []string{"/api", "/apis"} + + // Use a slice to preserve order from the peer. + // Use a map to track seen groups to avoid duplicates. + groupList := make([]apidiscoveryv2.APIGroupDiscovery, 0) + seenGroups := make(map[string]struct{}) + + for _, path := range discoveryPaths { + discoveryResponse, discoveryErr = h.aggregateDiscovery(ctx, path, hostport) + if discoveryErr != nil { + peerproxymetrics.IncPeerDiscoverySyncError(ctx, peerproxymetrics.DiscoveryErrorFetch) + klog.ErrorS(discoveryErr, "error querying discovery endpoint for serverID", "path", path, "serverID", serverID) + continue + } + + for _, groupDiscovery := range discoveryResponse.Items { + for _, version := range groupDiscovery.Versions { + for _, resource := range version.Resources { + gvr := schema.GroupVersionResource{ + Group: groupDiscovery.Name, + Version: version.Version, + Resource: resource.Resource, + } + gvrMap[gvr] = true + } + } + // Skip core/v1 group from peer-aggregated discovery since its not served from /apis. + // We still want to re-route core/v1 requests to the peer, but we don't want it + // to appear in the peer-aggregated discovery document. + if groupDiscovery.Name == "" { + continue + } + if _, ok := seenGroups[groupDiscovery.Name]; !ok { + groupList = append(groupList, groupDiscovery) + seenGroups[groupDiscovery.Name] = struct{}{} + } + } + } + + klog.V(4).Infof("Agg discovery done successfully by %s for %s", h.serverID, serverID) + return PeerDiscoveryCacheEntry{ + GVRs: gvrMap, + GroupDiscovery: groupList, + }, discoveryErr +} + +func (h *peerProxyHandler) aggregateDiscovery(ctx context.Context, path string, hostport string) (*apidiscoveryv2.APIGroupDiscoveryList, error) { + req, err := http.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, err + } + + apiServerUser := &user.DefaultInfo{ + Name: user.APIServerUser, + UID: user.APIServerUser, + Groups: []string{user.AllAuthenticated}, + } + + ctx = apirequest.WithUser(ctx, apiServerUser) + req = req.WithContext(ctx) + + // Fallback to V2 and V1 in that order if V2Local is not recognized. + req.Header.Add("Accept", discovery.AcceptV2NoPeer+","+discovery.AcceptV2+","+discovery.AcceptV1) + + writer := responsewriterutil.NewInMemoryResponseWriter() + h.proxyRequestToDestinationAPIServer(req, writer, hostport, schema.GroupVersionResource{}) + if writer.RespCode() != http.StatusOK { + return nil, fmt.Errorf("discovery request failed with status: %d", writer.RespCode()) + } + + parsed := &apidiscoveryv2.APIGroupDiscoveryList{} + if err := runtime.DecodeInto(h.discoverySerializer.UniversalDecoder(), writer.Data(), parsed); err != nil { + return nil, fmt.Errorf("error decoding discovery response: %w", err) + } + + return parsed, nil +} + +// handleSyncPeerDiscoveryCacheErr checks if an error happened during peer discovery sync and makes sure we will retry later. +func (h *peerProxyHandler) handleSyncPeerDiscoveryCacheErr(err error, key string) { + if err == nil { + h.peerLeaseQueue.Forget(key) + return + } + + if h.peerLeaseQueue.NumRequeues(key) < maxRetries { + klog.Infof("Error syncing discovery for peer lease %v: %v", key, err) + h.peerLeaseQueue.AddRateLimited(key) + return + } + + h.peerLeaseQueue.Forget(key) + utilruntime.HandleError(err) + klog.Infof("Dropping lease %s out of the queue: %v", key, err) +} + +func (h *peerProxyHandler) isValidPeerIdentityLease(obj interface{}) (*v1.Lease, bool) { + lease, ok := obj.(*v1.Lease) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + utilruntime.HandleError(fmt.Errorf("unexpected object type: %T", obj)) + return nil, false + } + if lease, ok = tombstone.Obj.(*v1.Lease); !ok { + utilruntime.HandleError(fmt.Errorf("unexpected object type: %T", obj)) + return nil, false + } + } + + if lease == nil { + klog.Error(fmt.Errorf("nil lease object provided")) + return nil, false + } + + if h.identityLeaseLabelSelector != nil && h.identityLeaseLabelSelector.String() != "" { + identityLeaseLabel := strings.Split(h.identityLeaseLabelSelector.String(), "=") + if len(identityLeaseLabel) != 2 { + klog.Errorf("invalid identityLeaseLabelSelector format: %s", h.identityLeaseLabelSelector.String()) + return nil, false + } + + if lease.Labels == nil || lease.Labels[identityLeaseLabel[0]] != identityLeaseLabel[1] { + klog.V(4).Infof("lease %s/%s does not match label selector: %s=%s", lease.Namespace, lease.Name, identityLeaseLabel[0], identityLeaseLabel[1]) + return nil, false + } + + } + + // Ignore self. + if lease.Name == h.serverID { + return nil, false + } + + if lease.Spec.HolderIdentity == nil { + klog.Error(fmt.Errorf("invalid lease object provided, missing holderIdentity in lease obj")) + return nil, false + } + + return lease, true +} + +func (h *peerProxyHandler) findServiceableByPeerFromPeerDiscoveryCache(gvr schema.GroupVersionResource) []string { + var serviceableByIDs []string + cacheMap := h.gvExclusionManager.GetFilteredPeerDiscoveryCache() + if len(cacheMap) == 0 { + return serviceableByIDs + } + + for peerID, peerData := range cacheMap { + // Ignore local apiserver. + if peerID == h.serverID { + continue + } + if _, exists := peerData.GVRs[gvr]; exists { + serviceableByIDs = append(serviceableByIDs, peerID) + } + } + return serviceableByIDs +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery_test.go similarity index 56% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery_test.go index 94cefd6df..d1cfc7b01 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peer_discovery_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peer_discovery_test.go @@ -22,6 +22,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -36,23 +37,24 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/transport" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" apidiscoveryv2 "k8s.io/api/apidiscovery/v2" v1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + peerproxymetrics "k8s.io/apiserver/pkg/util/peerproxy/metrics" ) func TestRunPeerDiscoveryCacheSync(t *testing.T) { - localServerID := "local-server" - testCases := []struct { desc string leases []*v1.Lease labelSelectorString string updatedLease *v1.Lease deletedLeaseNames []string - wantCache map[string]map[schema.GroupVersionResource]bool + wantCache map[string]PeerDiscoveryCacheEntry }{ { desc: "single remote server", @@ -66,10 +68,8 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { Spec: v1.LeaseSpec{HolderIdentity: proto.String("holder-1")}, }, }, - wantCache: map[string]map[schema.GroupVersionResource]bool{ - "remote-1": { - {Group: "testgroup", Version: "v1", Resource: "testresources"}: true, - }, + wantCache: map[string]PeerDiscoveryCacheEntry{ + "remote-1": makePeerDiscoveryCacheEntry("testgroup", "v1", "testresources"), }, }, { @@ -91,13 +91,9 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { Spec: v1.LeaseSpec{HolderIdentity: proto.String("holder-2")}, }, }, - wantCache: map[string]map[schema.GroupVersionResource]bool{ - "remote-1": { - {Group: "testgroup", Version: "v1", Resource: "testresources"}: true, - }, - "remote-2": { - {Group: "testgroup", Version: "v1", Resource: "testresources"}: true, - }, + wantCache: map[string]PeerDiscoveryCacheEntry{ + "remote-1": makePeerDiscoveryCacheEntry("testgroup", "v1", "testresources"), + "remote-2": makePeerDiscoveryCacheEntry("testgroup", "v1", "testresources"), }, }, { @@ -119,10 +115,8 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { }, Spec: v1.LeaseSpec{HolderIdentity: proto.String("holder-2")}, }, - wantCache: map[string]map[schema.GroupVersionResource]bool{ - "remote-1": { - {Group: "testgroup", Version: "v1", Resource: "testresources"}: true, - }, + wantCache: map[string]PeerDiscoveryCacheEntry{ + "remote-1": makePeerDiscoveryCacheEntry("testgroup", "v1", "testresources"), }, }, { @@ -138,37 +132,13 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { }, }, deletedLeaseNames: []string{"remote-1"}, - wantCache: map[string]map[schema.GroupVersionResource]bool{}, + wantCache: map[string]PeerDiscoveryCacheEntry{}, }, } for _, tt := range testCases { t.Run(tt.desc, func(t *testing.T) { - fakeClient := fake.NewSimpleClientset() - fakeInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0) - leaseInformer := fakeInformerFactory.Coordination().V1().Leases() - - fakeReconciler := newFakeReconciler() - - negotiatedSerializer := serializer.NewCodecFactory(runtime.NewScheme()) - loopbackConfig := &rest.Config{} - proxyConfig := &transport.Config{ - TLS: transport.TLSConfig{Insecure: true}, - } - - h, err := NewPeerProxyHandler( - localServerID, - tt.labelSelectorString, - leaseInformer, - fakeReconciler, - negotiatedSerializer, - loopbackConfig, - proxyConfig, - ) - if err != nil { - t.Fatalf("failed to create handler: %v", err) - } - + h, fakeReconciler, leaseInformer, fakeClient, fakeInformerFactory := setupPeerProxyHandler(t, tt.labelSelectorString) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -178,13 +148,13 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { if err != nil { t.Fatalf("failed to create lease: %v", err) } - if err = leaseInformer.Informer().GetIndexer().Add(lease); err != nil { + if err = leaseInformer.GetIndexer().Add(lease); err != nil { t.Fatalf("failed to create lease: %v", err) } } go fakeInformerFactory.Start(ctx.Done()) - cache.WaitForCacheSync(ctx.Done(), leaseInformer.Informer().HasSynced) + cache.WaitForCacheSync(ctx.Done(), leaseInformer.HasSynced) // Create test servers based on leases testServers := make(map[string]*httptest.Server) @@ -200,21 +170,20 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { } go h.RunPeerDiscoveryCacheSync(ctx, 1) + go h.RunPeerDiscoveryRefilter(ctx) // Wait for initial cache update. - initialCache := map[string]map[schema.GroupVersionResource]bool{} + initialCache := map[string]PeerDiscoveryCacheEntry{} for _, lease := range tt.leases { - initialCache[lease.Name] = map[schema.GroupVersionResource]bool{ - {Group: "testgroup", Version: "v1", Resource: "testresources"}: true, - } + initialCache[lease.Name] = makePeerDiscoveryCacheEntry("testgroup", "v1", "testresources") } - err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, false, func(ctx context.Context) (bool, error) { + err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, false, func(ctx context.Context) (bool, error) { select { case <-ctx.Done(): return false, ctx.Err() default: } - gotCache := h.peerDiscoveryInfoCache.Load() + gotCache := h.gvExclusionManager.GetFilteredPeerDiscoveryCache() return assert.ObjectsAreEqual(initialCache, gotCache), nil }) if err != nil { @@ -228,7 +197,7 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { if err != nil { t.Fatalf("failed to update lease: %v", err) } - if err = leaseInformer.Informer().GetIndexer().Update(updatedLease); err != nil { + if err = leaseInformer.GetIndexer().Update(updatedLease); err != nil { t.Fatalf("failed to update lease: %v", err) } } @@ -236,7 +205,7 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { // Delete leases if indicated. if len(tt.deletedLeaseNames) > 0 { for _, leaseName := range tt.deletedLeaseNames { - lease, exists, err := leaseInformer.Informer().GetIndexer().GetByKey("default/" + leaseName) + lease, exists, err := leaseInformer.GetIndexer().GetByKey("default/" + leaseName) if err != nil { t.Fatalf("failed to get lease from indexer: %v", err) } @@ -248,7 +217,7 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { if err != nil { t.Fatalf("failed to delete lease: %v", err) } - if err = leaseInformer.Informer().GetIndexer().Delete(deletedLease); err != nil { + if err = leaseInformer.GetIndexer().Delete(deletedLease); err != nil { t.Fatalf("failed to delete lease: %v", err) } @@ -261,8 +230,9 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { return false, ctx.Err() default: } - gotCache := h.peerDiscoveryInfoCache.Load() - return assert.ObjectsAreEqual(tt.wantCache, gotCache), nil + gotCache := h.gvExclusionManager.GetFilteredPeerDiscoveryCache() + r := assert.ObjectsAreEqual(tt.wantCache, gotCache) + return r, nil }) if err != nil { t.Errorf("cache doesnt match expectation: %v", err) @@ -272,6 +242,118 @@ func TestRunPeerDiscoveryCacheSync(t *testing.T) { } } +func TestPeerDiscoveryMetrics(t *testing.T) { + testCases := []struct { + desc string + leases []*v1.Lease + peerServerConfig map[string]http.HandlerFunc + wantMetrics string + }{ + { + desc: "hostport resolution error", + leases: []*v1.Lease{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "remote-resolution-error", + Labels: map[string]string{"apiserver-identity": "testserver"}, + }, + Spec: v1.LeaseSpec{HolderIdentity: proto.String("holder-error")}, + }, + }, + // No peer server configured means no endpoint will be registered in the reconciler. + // This should cause GetEndpoint to fail, triggering the "hostport_resolution" error. + peerServerConfig: nil, + wantMetrics: ` + # HELP apiserver_peer_discovery_sync_errors_total [ALPHA] Total number of errors encountered while syncing discovery information from a peer kube-apiserver + # TYPE apiserver_peer_discovery_sync_errors_total counter + apiserver_peer_discovery_sync_errors_total{type="hostport_resolution"} 1 + `, + }, + { + desc: "fetch discovery error", + leases: []*v1.Lease{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "remote-fetch-error", + Labels: map[string]string{"apiserver-identity": "testserver"}, + }, + Spec: v1.LeaseSpec{HolderIdentity: proto.String("holder-fetch-error")}, + }, + }, + peerServerConfig: map[string]http.HandlerFunc{ + "remote-fetch-error": func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + }, + wantMetrics: ` + # HELP apiserver_peer_discovery_sync_errors_total [ALPHA] Total number of errors encountered while syncing discovery information from a peer kube-apiserver + # TYPE apiserver_peer_discovery_sync_errors_total counter + apiserver_peer_discovery_sync_errors_total{type="fetch_discovery"} 2 + `, + }, + } + + peerproxymetrics.Register() + for _, tt := range testCases { + t.Run(tt.desc, func(t *testing.T) { + defer peerproxymetrics.Reset() + h, fakeReconciler, leaseInformer, _, _ := setupPeerProxyHandler(t, "apiserver-identity=testserver") + + for _, lease := range tt.leases { + if err := leaseInformer.GetIndexer().Add(lease); err != nil { + t.Fatalf("failed to create lease: %v", err) + } + } + + for leaseName, handler := range tt.peerServerConfig { + if handler == nil { + handler = func(w http.ResponseWriter, r *http.Request) {} + } + ts := httptest.NewServer(handler) + defer ts.Close() + fakeReconciler.setEndpoint(leaseName, ts.URL[7:]) + } + + // Directly call syncPeerDiscoveryCache + // We don't care about the return error of syncPeerDiscoveryCache for this test, + // we only care that metrics are incremented. + _ = h.syncPeerDiscoveryCache(context.Background()) + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.wantMetrics), "apiserver_peer_discovery_sync_errors_total"); err != nil { + t.Error(err) + } + }) + } +} + +func setupPeerProxyHandler(t *testing.T, labelSelector string) (*peerProxyHandler, *fakeReconciler, cache.SharedIndexInformer, *fake.Clientset, informers.SharedInformerFactory) { + localServerID := "local-server" + fakeClient := fake.NewSimpleClientset() + fakeInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0) + leaseInformer := fakeInformerFactory.Coordination().V1().Leases() + + fakeReconciler := newFakeReconciler() + negotiatedSerializer := serializer.NewCodecFactory(runtime.NewScheme()) + loopbackConfig := &rest.Config{} + proxyConfig := &transport.Config{ + TLS: transport.TLSConfig{Insecure: true}, + } + + h, err := NewPeerProxyHandler( + localServerID, + labelSelector, + leaseInformer, + fakeReconciler, + negotiatedSerializer, + loopbackConfig, + proxyConfig, + ) + if err != nil { + t.Fatalf("failed to create handler: %v", err) + } + + return h, fakeReconciler, leaseInformer.Informer(), fakeClient, fakeInformerFactory +} + // newTestTLSServer creates a new httptest.NewTLSServer that serves discovery endpoints. func newTestTLSServer(t *testing.T) *httptest.Server { return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -342,3 +424,26 @@ func (f *fakeReconciler) StopReconciling() { func (f *fakeReconciler) setEndpoint(serverID, endpoint string) { f.endpoints[serverID] = endpoint } + +func makePeerDiscoveryCacheEntry(group, version, resource string) PeerDiscoveryCacheEntry { + return PeerDiscoveryCacheEntry{ + GVRs: map[schema.GroupVersionResource]bool{ + {Group: group, Version: version, Resource: resource}: true, + }, + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: group, + }, + Versions: []apidiscoveryv2.APIVersionDiscovery{ + { + Version: version, + Resources: []apidiscoveryv2.APIResourceDiscovery{ + {Resource: resource}, + }, + }, + }, + }, + }, + } +} diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy.go new file mode 100644 index 000000000..9644be4cf --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy.go @@ -0,0 +1,233 @@ +/* +Copyright 2023 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 peerproxy + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync/atomic" + "time" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apiserver/pkg/reconcilers" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/transport" + "k8s.io/client-go/util/workqueue" + + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" + schema "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + coordinationv1informers "k8s.io/client-go/informers/coordination/v1" +) + +const ( + // localDiscoveryRefreshInterval is the interval at which the local discovery cache is refreshed. + // This cache is only used for mixed version proxy routing decisions (shouldServeLocally), not for serving discovery responses. + // Periodic refreshes ensure that we prefer serving requests locally when possible. Without this refresh, + // a stale cache could cause us to proxy a request to a peer even when we can serve it locally, resulting + // in unnecessary network hops. This is particularly important during upgrades when new built-in APIs become + // available on this server. + localDiscoveryRefreshInterval = 30 * time.Minute + // defaultExclusionGracePeriod is the default duration to wait before + // removing a groupversion from the exclusion set after it is deleted from + // CRDs and aggregated APIs. + // This is to allow time for all peer API servers to also observe + // the deleted CRD or aggregated API before this server stops excluding it + // in peer-aggregated discovery and while proxying requests to peers. + defaultExclusionGracePeriod = 5 * time.Minute +) + +// Interface defines how the Mixed Version Proxy filter interacts with the underlying system. +type Interface interface { + WrapHandler(handler http.Handler) http.Handler + WaitForCacheSync(stopCh <-chan struct{}) error + HasFinishedSync() bool + RunLocalDiscoveryCacheSync(stopCh <-chan struct{}) error + RunPeerDiscoveryCacheSync(ctx context.Context, workers int) + GetPeerResources() map[string][]apidiscoveryv2.APIGroupDiscovery + RegisterCacheInvalidationCallback(cb func()) + + // RegisterCRDInformerHandlers registers event handlers on the CRD informer to track + // which GroupVersions are served locally by CRDs. When a CRD is created or updated, + // its GV is added to the exclusion set. When deleted, the GV is marked for exclusion + // during a grace period to allow peers to observe the deletion. The extractor function + // extracts the GroupVersion from a CRD object. + // + // This exclusion is necessary because peer discovery is not refreshed when a local + // CRD is deleted. Without exclusion, the deleted GV might still appear in cached peer + // discovery data, causing requests to be incorrectly routed to a peer for a GV that + // no longer exists locally. Therefore, we intentionally exclude CRD GVs from peer + // discovery from the start and only rely on the local apiserver's view of the CRD + // to serve it in peer-aggregated discovery. + RegisterCRDInformerHandlers(crdInformer cache.SharedIndexInformer, extractor GVExtractor) error + + // RegisterAPIServiceInformerHandlers registers event handlers on the APIService informer + // to track which GroupVersions are served locally by aggregated APIServices. When an + // APIService is created or updated, its GV is added to the exclusion set. When deleted, + // the GV is marked for exclusion during a grace period. + // + // This exclusion is necessary because peer discovery is not refreshed when a local + // aggregated APIService is deleted. Without exclusion, the deleted GV might still appear + // in cached peer discovery data, causing requests to be incorrectly routed to a peer. + // Therefore, we intentionally exclude aggregated APIService GVs from peer discovery + // from the start and only rely on the local apiserver's view to serve them in + // peer-aggregated discovery. + RegisterAPIServiceInformerHandlers(apiServiceInformer cache.SharedIndexInformer, extractor GVExtractor) error + + // RunPeerDiscoveryActiveGVTracker starts a worker that processes CRD/APIService informer + // events to rebuild the set of actively served GroupVersions. This worker is triggered + // whenever a CRD or APIService is added or updated and updates the exclusion + // set accordingly. When a GV is deleted, a delayed sync is automatically scheduled + // after the grace period to reap expired GVs from the exclusion set. + RunPeerDiscoveryActiveGVTracker(ctx context.Context) + + // RunPeerDiscoveryRefilter starts a worker that re-applies exclusion filtering to the + // cached peer discovery data whenever the exclusion set changes. This ensures that + // already-cached peer discovery responses are immediately updated to exclude newly added + // or updated local GVs, rather than waiting for the next peer lease event to trigger a + // cache refresh of peer discovery data. + RunPeerDiscoveryRefilter(ctx context.Context) +} + +// New creates a new instance to implement unknown version proxy +// This method is used for an alpha feature UnknownVersionInteroperabilityProxy +// and is subject to future modifications. +func NewPeerProxyHandler( + serverId string, + identityLeaseLabelSelector string, + leaseInformer coordinationv1informers.LeaseInformer, + reconciler reconcilers.PeerEndpointLeaseReconciler, + ser runtime.NegotiatedSerializer, + loopbackClientConfig *rest.Config, + proxyClientConfig *transport.Config, +) (*peerProxyHandler, error) { + h := &peerProxyHandler{ + name: "PeerProxyHandler", + serverID: serverId, + reconciler: reconciler, + serializer: ser, + localDiscoveryInfoCache: atomic.Value{}, + localDiscoveryCacheTicker: time.NewTicker(localDiscoveryRefreshInterval), + localDiscoveryInfoCachePopulated: make(chan struct{}), + rawPeerDiscoveryCache: atomic.Value{}, + peerLeaseQueue: workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.DefaultTypedControllerRateLimiter[string](), + workqueue.TypedRateLimitingQueueConfig[string]{ + Name: peerDiscoveryControllerName, + }), + apiserverIdentityInformer: leaseInformer, + } + + h.gvExclusionManager = NewGVExclusionManager( + defaultExclusionGracePeriod, + &h.rawPeerDiscoveryCache, + &h.cacheInvalidationCallback, + ) + + if parts := strings.Split(identityLeaseLabelSelector, "="); len(parts) != 2 { + return nil, fmt.Errorf("invalid identityLeaseLabelSelector provided, must be of the form key=value, received: %v", identityLeaseLabelSelector) + } + selector, err := labels.Parse(identityLeaseLabelSelector) + if err != nil { + return nil, fmt.Errorf("failed to parse label selector: %w", err) + } + h.identityLeaseLabelSelector = selector + + discoveryScheme := runtime.NewScheme() + utilruntime.Must(apidiscoveryv2.AddToScheme(discoveryScheme)) + h.discoverySerializer = serializer.NewCodecFactory(discoveryScheme) + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(loopbackClientConfig) + if err != nil { + return nil, fmt.Errorf("error creating discovery client: %w", err) + } + + // Always use local discovery to get local view of resources. + discoveryClient.NoPeerDiscovery = true + h.discoveryClient = discoveryClient + h.localDiscoveryInfoCache.Store(map[schema.GroupVersionResource]bool{}) + h.rawPeerDiscoveryCache.Store(map[string]PeerDiscoveryCacheEntry{}) + + proxyTransport, err := transport.New(proxyClientConfig) + if err != nil { + return nil, fmt.Errorf("failed to create proxy transport: %w", err) + } + h.proxyTransport = proxyTransport + + peerDiscoveryRegistration, err := h.apiserverIdentityInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + if lease, ok := h.isValidPeerIdentityLease(obj); ok { + h.enqueueLease(lease) + } + }, + UpdateFunc: func(oldObj, newObj interface{}) { + oldLease, oldLeaseOk := h.isValidPeerIdentityLease(oldObj) + newLease, newLeaseOk := h.isValidPeerIdentityLease(newObj) + if oldLeaseOk && newLeaseOk && + oldLease.Name == newLease.Name && *oldLease.Spec.HolderIdentity != *newLease.Spec.HolderIdentity { + h.enqueueLease(newLease) + } + }, + DeleteFunc: func(obj interface{}) { + if lease, ok := h.isValidPeerIdentityLease(obj); ok { + h.enqueueLease(lease) + } + }, + }) + if err != nil { + return nil, err + } + + h.leaseRegistration = peerDiscoveryRegistration + return h, nil +} + +// RegisterCRDInformerHandlers registers event handlers for CRD informer. +func (h *peerProxyHandler) RegisterCRDInformerHandlers(crdInformer cache.SharedIndexInformer, extractor GVExtractor) error { + if h.gvExclusionManager != nil { + return h.gvExclusionManager.RegisterCRDInformerHandlers(crdInformer, extractor) + } + return nil +} + +// RegisterAPIServiceInformerHandlers registers event handlers for APIService informer. +func (h *peerProxyHandler) RegisterAPIServiceInformerHandlers(apiServiceInformer cache.SharedIndexInformer, extractor GVExtractor) error { + if h.gvExclusionManager != nil { + return h.gvExclusionManager.RegisterAPIServiceInformerHandlers(apiServiceInformer, extractor) + } + return nil +} + +// RunPeerDiscoveryActiveGVTracker starts the worker that tracks active GVs from CRDs/APIServices. +func (h *peerProxyHandler) RunPeerDiscoveryActiveGVTracker(ctx context.Context) { + if h.gvExclusionManager != nil { + h.gvExclusionManager.RunPeerDiscoveryActiveGVTracker(ctx) + } +} + +// RunPeerDiscoveryRefilter starts the worker that refilters peer discovery cache. +func (h *peerProxyHandler) RunPeerDiscoveryRefilter(ctx context.Context) { + if h.gvExclusionManager != nil { + h.gvExclusionManager.RunPeerDiscoveryRefilter(ctx) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler.go similarity index 68% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler.go index 289427134..97c2a2e28 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/proxy" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/responsewriter" @@ -42,9 +43,9 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" apierrors "k8s.io/apimachinery/pkg/api/errors" schema "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" epmetrics "k8s.io/apiserver/pkg/endpoints/metrics" apirequest "k8s.io/apiserver/pkg/endpoints/request" apiserverproxyutil "k8s.io/apiserver/pkg/util/proxy" @@ -60,6 +61,7 @@ type peerProxyHandler struct { // Identity for this server. serverID string finishedSync atomic.Bool + // Label to check against in identity leases to make sure // we are working with apiserver identity leases only. identityLeaseLabelSelector labels.Selector @@ -77,14 +79,23 @@ type peerProxyHandler struct { localDiscoveryCacheTicker *time.Ticker localDiscoveryInfoCachePopulated chan struct{} localDiscoveryInfoCachePopulatedOnce sync.Once - // Cache that stores resources served by peer apiservers. - // Refreshed if a new apiserver identity lease is added, deleted or - // holderIndentity change is observed in the lease. - peerDiscoveryInfoCache atomic.Value - proxyTransport http.RoundTripper - // Worker queue that keeps the peerDiscoveryInfoCache up-to-date. - peerLeaseQueue workqueue.TypedRateLimitingInterface[string] - serializer runtime.NegotiatedSerializer + // rawPeerDiscoveryCache stores unfiltered resources and groups served by peer apiservers. + // The map is from string (serverID) to PeerDiscoveryCacheEntry. + // Written ONLY by peerLeaseQueue worker when peer leases change. + rawPeerDiscoveryCache atomic.Value // map[string]PeerDiscoveryCacheEntry + proxyTransport http.RoundTripper + // Worker queue that keeps the rawPeerDiscoveryCache up-to-date. + peerLeaseQueue workqueue.TypedRateLimitingInterface[string] + serializer runtime.NegotiatedSerializer + cacheInvalidationCallback atomic.Pointer[func()] + // Manager for GV exclusions (CRDs/APIServices) + gvExclusionManager *GVExclusionManager +} + +// PeerDiscoveryCacheEntry holds the GVRs and group-level discovery info for a peer. +type PeerDiscoveryCacheEntry struct { + GVRs map[schema.GroupVersionResource]bool + GroupDiscovery []apidiscoveryv2.APIGroupDiscovery } // responder implements rest.Responder for assisting a connector in writing objects or errors. @@ -93,6 +104,10 @@ type responder struct { ctx context.Context } +func (h *peerProxyHandler) RegisterCacheInvalidationCallback(cb func()) { + h.cacheInvalidationCallback.Store(&cb) +} + func (h *peerProxyHandler) HasFinishedSync() bool { return h.finishedSync.Load() } @@ -103,10 +118,16 @@ func (h *peerProxyHandler) WaitForCacheSync(stopCh <-chan struct{}) error { return fmt.Errorf("error while waiting for initial cache sync") } - if !cache.WaitForNamedCacheSync(controllerName, stopCh, h.leaseRegistration.HasSynced) { + if !cache.WaitForNamedCacheSync(peerDiscoveryControllerName, stopCh, h.leaseRegistration.HasSynced) { return fmt.Errorf("error while waiting for peer-identity-lease event handler registration sync") } + if h.gvExclusionManager != nil { + if !h.gvExclusionManager.WaitForCacheSync(stopCh) { + return fmt.Errorf("error while waiting for gv exclusion manager cache sync") + } + } + // Wait for localDiscoveryInfoCache to be populated. select { case <-h.localDiscoveryInfoCachePopulated: @@ -152,9 +173,6 @@ func (h *peerProxyHandler) WrapHandler(handler http.Handler) http.Handler { } gvr := schema.GroupVersionResource{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion, Resource: requestInfo.Resource} - if requestInfo.APIGroup == "" { - gvr.Group = "core" - } if h.shouldServeLocally(gvr) { handler.ServeHTTP(w, r) @@ -164,118 +182,24 @@ func (h *peerProxyHandler) WrapHandler(handler http.Handler) http.Handler { // find servers that are capable of serving this request peerServerIDs := h.findServiceableByPeerFromPeerDiscoveryCache(gvr) if len(peerServerIDs) == 0 { - klog.Errorf("gvr %v is not served by anything in this cluster", gvr) + klog.V(3).Infof("gvr %v is not served by anything in this cluster", gvr) handler.ServeHTTP(w, r) return } peerEndpoints, err := h.resolveServingLocation(peerServerIDs) if err != nil { + metrics.IncPeerProxyError(ctx, metrics.ProxyErrorEndpointResolution, gvr.Group, gvr.Version, gvr.Resource) gv := schema.GroupVersion{Group: gvr.Group, Version: gvr.Version} klog.ErrorS(err, "error finding serviceable-by apiservers for the requested resource", "gvr", gvr) responsewriters.ErrorNegotiated(apierrors.NewServiceUnavailable("Error getting ip and port info of the remote server while proxying"), h.serializer, gv, w, r) return } - rand := rand.Intn(len(peerEndpoints)) - peerEndpoint := peerEndpoints[rand] - h.proxyRequestToDestinationAPIServer(r, w, peerEndpoint) - }) -} - -// RunLocalDiscoveryCacheSync populated the localDiscoveryInfoCache and -// starts a goroutine to periodically refresh the local discovery cache. -func (h *peerProxyHandler) RunLocalDiscoveryCacheSync(stopCh <-chan struct{}) error { - klog.Info("localDiscoveryCacheInvalidation goroutine started") - // Populate the cache initially. - if err := h.populateLocalDiscoveryCache(); err != nil { - return fmt.Errorf("failed to populate initial local discovery cache: %w", err) - } - - go func() { - for { - select { - case <-h.localDiscoveryCacheTicker.C: - klog.V(4).Infof("Invalidating local discovery cache") - if err := h.populateLocalDiscoveryCache(); err != nil { - klog.Errorf("Failed to repopulate local discovery cache: %v", err) - } - case <-stopCh: - klog.Info("localDiscoveryCacheInvalidation goroutine received stop signal") - if h.localDiscoveryCacheTicker != nil { - h.localDiscoveryCacheTicker.Stop() - klog.Info("localDiscoveryCacheTicker stopped") - } - klog.Info("localDiscoveryCacheInvalidation goroutine exiting") - return - } - } - }() - return nil -} - -func (h *peerProxyHandler) populateLocalDiscoveryCache() error { - _, resourcesByGV, _, err := h.discoveryClient.GroupsAndMaybeResources() - if err != nil { - return fmt.Errorf("error getting API group resources from discovery: %w", err) - } - - freshLocalDiscoveryResponse := map[schema.GroupVersionResource]bool{} - for gv, resources := range resourcesByGV { - if gv.Group == "" { - gv.Group = "core" - } - for _, resource := range resources.APIResources { - gvr := gv.WithResource(resource.Name) - freshLocalDiscoveryResponse[gvr] = true - } - } - - h.localDiscoveryInfoCache.Store(freshLocalDiscoveryResponse) - // Signal that the cache has been populated. - h.localDiscoveryInfoCachePopulatedOnce.Do(func() { - close(h.localDiscoveryInfoCachePopulated) + endpointIndex := rand.Intn(len(peerEndpoints)) + peerEndpoint := peerEndpoints[endpointIndex] + h.proxyRequestToDestinationAPIServer(r, w, peerEndpoint, gvr) }) - return nil -} - -// shouldServeLocally checks if the requested resource is present in the local -// discovery cache indicating the request can be served by this server. -func (h *peerProxyHandler) shouldServeLocally(gvr schema.GroupVersionResource) bool { - cache := h.localDiscoveryInfoCache.Load().(map[schema.GroupVersionResource]bool) - exists, ok := cache[gvr] - if !ok { - klog.V(4).Infof("resource not found for %v in local discovery cache\n", gvr.GroupVersion()) - return false - } - - if exists { - return true - } - - return false -} - -func (h *peerProxyHandler) findServiceableByPeerFromPeerDiscoveryCache(gvr schema.GroupVersionResource) []string { - var serviceableByIDs []string - cache := h.peerDiscoveryInfoCache.Load().(map[string]map[schema.GroupVersionResource]bool) - for peerID, servedResources := range cache { - // Ignore local apiserver. - if peerID == h.serverID { - continue - } - - exists, ok := servedResources[gvr] - if !ok { - continue - } - - if exists { - serviceableByIDs = append(serviceableByIDs, peerID) - } - } - - return serviceableByIDs } // resolveServingLocation resolves the host:port addresses for the given peer IDs. @@ -315,7 +239,7 @@ func (h *peerProxyHandler) hostportInfo(apiserverKey string) (string, error) { return hostPort, nil } -func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, rw http.ResponseWriter, host string) { +func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, rw http.ResponseWriter, host string, gvr schema.GroupVersionResource) { // write a new location based on the existing request pointed at the target service location := &url.URL{} location.Scheme = "https" @@ -329,6 +253,7 @@ func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, proxyRoundTripper, err := h.buildProxyRoundtripper(req) if err != nil { + metrics.IncPeerProxyError(req.Context(), metrics.ProxyErrorTransport, gvr.Group, gvr.Version, gvr.Resource) klog.Errorf("failed to build proxy round tripper: %v", err) return } @@ -336,8 +261,9 @@ func (h *peerProxyHandler) proxyRequestToDestinationAPIServer(req *http.Request, delegate := &epmetrics.ResponseWriterDelegator{ResponseWriter: rw} w := responsewriter.WrapForHTTP1Or2(delegate) handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, false, &responder{w: w, ctx: req.Context()}) + klog.Infof("Proxying request for %s from %s to %s", req.URL.Path, req.Host, location.Host) handler.ServeHTTP(w, newReq) - metrics.IncPeerProxiedRequest(req.Context(), strconv.Itoa(delegate.Status())) + metrics.IncPeerProxiedRequest(req.Context(), strconv.Itoa(delegate.Status()), gvr.Group, gvr.Version, gvr.Resource) } func (h *peerProxyHandler) buildProxyRoundtripper(req *http.Request) (http.RoundTripper, error) { @@ -353,3 +279,24 @@ func (r *responder) Error(w http.ResponseWriter, req *http.Request, err error) { klog.ErrorS(err, "Error while proxying request to destination apiserver") http.Error(w, err.Error(), http.StatusServiceUnavailable) } + +// GetPeerResources implements PeerDiscoveryProvider interface +// Returns a map of serverID -> []apidiscoveryv2.APIGroupDiscovery served by peer servers +func (h *peerProxyHandler) GetPeerResources() map[string][]apidiscoveryv2.APIGroupDiscovery { + result := make(map[string][]apidiscoveryv2.APIGroupDiscovery) + cacheMap := h.gvExclusionManager.GetFilteredPeerDiscoveryCache() + if len(cacheMap) == 0 { + klog.V(4).Infof("GetPeerResources: peer cache is empty") + return result + } + + for serverID, peerData := range cacheMap { + if serverID == h.serverID { + klog.V(4).Infof("GetPeerResources: skipping local server %s", serverID) + continue // Skip local server + } + result[serverID] = peerData.GroupDiscovery + } + + return result +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler_test.go similarity index 67% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler_test.go index f313678d4..01f90f07f 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/peerproxy/peerproxy_handler_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/peerproxy/peerproxy_handler_test.go @@ -42,6 +42,7 @@ import ( "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" + apidiscoveryv2 "k8s.io/api/apidiscovery/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -77,7 +78,7 @@ func TestPeerProxy(t *testing.T) { peerproxiedHeader string reconcilerConfig reconciler localCache map[schema.GroupVersionResource]bool - peerCache map[string]map[schema.GroupVersionResource]bool + peerCache map[string]PeerDiscoveryCacheEntry wantStatus int wantMetricsData string }{ @@ -102,7 +103,7 @@ func TestPeerProxy(t *testing.T) { desc: "Serve locally if serviceable", requestPath: "/api/foo/bar", localCache: map[schema.GroupVersionResource]bool{ - {Group: "core", Version: "foo", Resource: "bar"}: true, + {Group: "", Version: "foo", Resource: "bar"}: true, }, wantStatus: http.StatusOK, }, @@ -116,20 +117,29 @@ func TestPeerProxy(t *testing.T) { desc: "503 if no endpoint fetched from lease", requestPath: "/api/foo/bar", informerFinishedSync: true, - peerCache: map[string]map[schema.GroupVersionResource]bool{ + peerCache: map[string]PeerDiscoveryCacheEntry{ remoteServerID1: { - {Group: "core", Version: "foo", Resource: "bar"}: true, + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "", Version: "foo", Resource: "bar"}: true, + }, }, }, wantStatus: http.StatusServiceUnavailable, + wantMetricsData: ` + # HELP apiserver_peer_proxy_errors_total [ALPHA] Total number of errors encountered while proxying requests to a peer kube apiserver + # TYPE apiserver_peer_proxy_errors_total counter + apiserver_peer_proxy_errors_total{group="",resource="bar",type="endpoint_resolution",version="foo"} 1 + `, }, { desc: "503 unreachable peer bind address", requestPath: "/api/foo/bar", informerFinishedSync: true, - peerCache: map[string]map[schema.GroupVersionResource]bool{ + peerCache: map[string]PeerDiscoveryCacheEntry{ remoteServerID1: { - {Group: "core", Version: "foo", Resource: "bar"}: true, + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "", Version: "foo", Resource: "bar"}: true, + }, }, }, reconcilerConfig: reconciler{ @@ -143,21 +153,25 @@ func TestPeerProxy(t *testing.T) { }, wantStatus: http.StatusServiceUnavailable, wantMetricsData: ` - # HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it - # TYPE apiserver_rerouted_request_total counter - apiserver_rerouted_request_total{code="503"} 1 - `, + # HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube-apiserver because the local apiserver was not capable of serving it, broken down by 'group', 'version', and 'resource' indicating the GVR of the request. If all three are empty (""), the request is a discovery request. + # TYPE apiserver_rerouted_request_total counter + apiserver_rerouted_request_total{code="503",group="",resource="bar",version="foo"} 1 + `, }, { desc: "503 if one apiserver's endpoint lease wasnt found but another valid (unreachable) apiserver was found", requestPath: "/api/foo/bar", informerFinishedSync: true, - peerCache: map[string]map[schema.GroupVersionResource]bool{ + peerCache: map[string]PeerDiscoveryCacheEntry{ remoteServerID1: { - {Group: "core", Version: "foo", Resource: "bar"}: true, + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "", Version: "foo", Resource: "bar"}: true, + }, }, remoteServerID2: { - {Group: "core", Version: "foo", Resource: "bar"}: true, + GVRs: map[schema.GroupVersionResource]bool{ + {Group: "", Version: "foo", Resource: "bar"}: true, + }, }, }, reconcilerConfig: reconciler{ @@ -171,10 +185,10 @@ func TestPeerProxy(t *testing.T) { }, wantStatus: http.StatusServiceUnavailable, wantMetricsData: ` - # HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube apiserver because the local apiserver was not capable of serving it - # TYPE apiserver_rerouted_request_total counter - apiserver_rerouted_request_total{code="503"} 1 - `, + # HELP apiserver_rerouted_request_total [ALPHA] Total number of requests that were proxied to a peer kube-apiserver because the local apiserver was not capable of serving it, broken down by 'group', 'version', and 'resource' indicating the GVR of the request. If all three are empty (""), the request is a discovery request. + # TYPE apiserver_rerouted_request_total counter + apiserver_rerouted_request_total{code="503",group="",resource="bar",version="foo"} 1 + `, }, } @@ -221,7 +235,7 @@ func TestPeerProxy(t *testing.T) { // compare metric if tt.wantMetricsData != "" { - if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.wantMetricsData), []string{"apiserver_rerouted_request_total"}...); err != nil { + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tt.wantMetricsData), []string{"apiserver_rerouted_request_total", "apiserver_peer_proxy_errors_total"}...); err != nil { t.Fatal(err) } } @@ -230,12 +244,101 @@ func TestPeerProxy(t *testing.T) { } +func TestGetPeerResources(t *testing.T) { + testCases := []struct { + name string + peerCache map[string]PeerDiscoveryCacheEntry + want map[string][]apidiscoveryv2.APIGroupDiscovery + }{ + { + name: "empty peer cache", + peerCache: nil, + want: map[string][]apidiscoveryv2.APIGroupDiscovery{}, + }, + { + name: "peer cache with local server, should be skipped", + peerCache: map[string]PeerDiscoveryCacheEntry{ + localServerID: { + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "core"}, + }, + }, + }, + remoteServerID1: { + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + }, + }, + }, + }, + want: map[string][]apidiscoveryv2.APIGroupDiscovery{ + remoteServerID1: { + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + }, + }, + }, + }, + { + name: "peer cache with multiple peers", + peerCache: map[string]PeerDiscoveryCacheEntry{ + remoteServerID1: { + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + }, + }, + }, + remoteServerID2: { + GroupDiscovery: []apidiscoveryv2.APIGroupDiscovery{ + { + ObjectMeta: metav1.ObjectMeta{Name: "batch"}, + }, + }, + }, + }, + want: map[string][]apidiscoveryv2.APIGroupDiscovery{ + remoteServerID1: { + { + ObjectMeta: metav1.ObjectMeta{Name: "apps"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + }, + }, + remoteServerID2: { + { + ObjectMeta: metav1.ObjectMeta{Name: "batch"}, + }, + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion() + handler, err := newFakePeerProxyHandler(true, nil, localServerID, s, nil, tt.peerCache) + if err != nil { + t.Fatalf("Error creating peer proxy handler: %v", err) + } + + got := handler.GetPeerResources() + assert.Equal(t, tt.want, got) + }) + } +} + func newFakePeerEndpointReconciler(t *testing.T) reconcilers.PeerEndpointLeaseReconciler { server, sc := etcd3testing.NewUnsecuredEtcd3TestClientServer(t) t.Cleanup(func() { server.Terminate(t) }) scheme := runtime.NewScheme() metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion) - //utilruntime.Must(core.AddToScheme(scheme)) utilruntime.Must(corev1.AddToScheme(scheme)) utilruntime.Must(scheme.SetVersionPriority(corev1.SchemeGroupVersion)) codecs := serializer.NewCodecFactory(scheme) @@ -252,7 +355,7 @@ func newFakePeerEndpointReconciler(t *testing.T) reconcilers.PeerEndpointLeaseRe func newHandlerChain(t *testing.T, informerFinishedSync bool, handler http.Handler, reconciler reconcilers.PeerEndpointLeaseReconciler, - localCache map[schema.GroupVersionResource]bool, peerCache map[string]map[schema.GroupVersionResource]bool) http.Handler { + localCache map[schema.GroupVersionResource]bool, peerCache map[string]PeerDiscoveryCacheEntry) http.Handler { // Add peerproxy handler s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion() peerProxyHandler, err := newFakePeerProxyHandler(informerFinishedSync, reconciler, localServerID, s, localCache, peerCache) @@ -273,7 +376,7 @@ func newHandlerChain(t *testing.T, informerFinishedSync bool, handler http.Handl func newFakePeerProxyHandler(informerFinishedSync bool, reconciler reconcilers.PeerEndpointLeaseReconciler, id string, s runtime.NegotiatedSerializer, - localCache map[schema.GroupVersionResource]bool, peerCache map[string]map[schema.GroupVersionResource]bool) (*peerProxyHandler, error) { + localCache map[schema.GroupVersionResource]bool, peerCache map[string]PeerDiscoveryCacheEntry) (*peerProxyHandler, error) { clientset := fake.NewSimpleClientset() informerFactory := informers.NewSharedInformerFactory(clientset, 0) leaseInformer := informerFactory.Coordination().V1().Leases() @@ -289,7 +392,7 @@ func newFakePeerProxyHandler(informerFinishedSync bool, return nil, err } ppH.localDiscoveryInfoCache.Store(localCache) - ppH.peerDiscoveryInfoCache.Store(peerCache) + ppH.gvExclusionManager.filteredPeerDiscoveryCache.Store(peerCache) ppH.finishedSync.Store(informerFinishedSync) return ppH, nil diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/doc.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/endpointslice.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/endpointslice.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/endpointslice.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/endpointslice.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/endpointslice_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/endpointslice_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/endpointslice_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/endpointslice_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/metrics/metrics.go similarity index 67% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/metrics/metrics.go index e7e83c171..f09724716 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/metrics/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/metrics/metrics.go @@ -54,18 +54,33 @@ var ( }, []string{statuscode}, ) + // websocketStreamingRequestsTotal counts WebSocket streaming requests (exec/attach/portforward) + // routed by the API server, labeled by subresource and proxy_type. + websocketStreamingRequestsTotal = metrics.NewCounterVec( + &metrics.CounterOpts{ + Subsystem: subsystem, + Name: "websocket_streaming_requests_total", + Help: "Total number of WebSocket streaming requests (exec/attach/portforward) routed by the API server, " + + "labeled by subresource and proxy_type. proxy_type is proxied_to_kubelet when the kubelet " + + "handles the request directly; otherwise translated_at_apiserver.", + StabilityLevel: metrics.ALPHA, + }, + []string{"subresource", "proxy_type"}, + ) ) func Register() { registerMetricsOnce.Do(func() { legacyregistry.MustRegister(streamTranslatorRequestsTotal) legacyregistry.MustRegister(streamTunnelRequestsTotal) + legacyregistry.MustRegister(websocketStreamingRequestsTotal) }) } func ResetForTest() { streamTranslatorRequestsTotal.Reset() streamTunnelRequestsTotal.Reset() + websocketStreamingRequestsTotal.Reset() } // IncStreamTranslatorRequest increments the # of requests handled by the StreamTranslatorProxy. @@ -77,3 +92,10 @@ func IncStreamTranslatorRequest(ctx context.Context, status string) { func IncStreamTunnelRequest(ctx context.Context, status string) { streamTunnelRequestsTotal.WithContext(ctx).WithLabelValues(status).Add(1) } + +// IncWebSocketStreamingRequest increments the count of WebSocket streaming requests +// routed by the API server with the given subresource (exec/attach/portforward) and +// proxy_type (proxied_to_kubelet or translated_at_apiserver). +func IncWebSocketStreamingRequest(ctx context.Context, subresource, proxyType string) { + websocketStreamingRequestsTotal.WithContext(ctx).WithLabelValues(subresource, proxyType).Add(1) +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/proxy.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/proxy.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/proxy.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/proxy.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/proxy_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/proxy_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/proxy_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/proxy_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator.go similarity index 97% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator.go index 33bbdc325..a85a5c5d0 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator.go @@ -26,11 +26,12 @@ import ( "github.com/mxk/go-flowrate/flowrate" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/httpstream/spdy" constants "k8s.io/apimachinery/pkg/util/remotecommand" "k8s.io/apiserver/pkg/util/proxy/metrics" "k8s.io/client-go/tools/remotecommand" + clientspdy "k8s.io/client-go/transport/spdy" "k8s.io/client-go/util/exec" + "k8s.io/streaming/pkg/httpstream/spdy" ) // StreamTranslatorHandler is a handler which translates WebSocket stream data @@ -77,7 +78,12 @@ func (h *StreamTranslatorHandler) ServeHTTP(w http.ResponseWriter, req *http.Req websocketStreams.writeStatus(apierrors.NewInternalError(err)) //nolint:errcheck return } - spdyExecutor, err := remotecommand.NewSPDYExecutorRejectRedirects(spdyRoundTripper, spdyRoundTripper, "POST", h.Location) + spdyExecutor, err := remotecommand.NewSPDYExecutorRejectRedirects( + spdyRoundTripper, + clientspdy.NewUpgraderForStreaming(spdyRoundTripper), + "POST", + h.Location, + ) if err != nil { metrics.IncStreamTranslatorRequest(req.Context(), strconv.Itoa(http.StatusInternalServerError)) websocketStreams.writeStatus(apierrors.NewInternalError(err)) //nolint:errcheck diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator_test.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator_test.go index c34522022..ffbd0f635 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtranslator_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtranslator_test.go @@ -28,6 +28,7 @@ import ( mrand "math/rand" "net/http" "net/http/httptest" + "net/http/httputil" "net/url" "reflect" "strings" @@ -37,8 +38,6 @@ import ( v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/httpstream" - "k8s.io/apimachinery/pkg/util/httpstream/spdy" rcconstants "k8s.io/apimachinery/pkg/util/remotecommand" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/util/proxy/metrics" @@ -47,6 +46,8 @@ import ( "k8s.io/client-go/transport" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" + "k8s.io/streaming/pkg/httpstream" + "k8s.io/streaming/pkg/httpstream/spdy" ) // TestStreamTranslator_LoopbackStdinToStdout returns random data sent on the client's @@ -61,7 +62,7 @@ func TestStreamTranslator_LoopbackStdinToStdout(t *testing.T) { t.Cleanup(metrics.ResetForTest) // Create upstream fake SPDY server which copies STDIN back onto STDOUT stream. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Stdin: true, Stdout: true, }) @@ -162,7 +163,7 @@ func TestStreamTranslator_LoopbackStdinToStderr(t *testing.T) { t.Cleanup(metrics.ResetForTest) // Create upstream fake SPDY server which copies STDIN back onto STDERR stream. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Stdin: true, Stderr: true, }) @@ -267,7 +268,7 @@ func TestStreamTranslator_ErrorStream(t *testing.T) { // Create upstream fake SPDY server, returning a non-zero exit code // on error stream within the structured error. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Stdout: true, }) if err != nil { @@ -373,7 +374,7 @@ func TestStreamTranslator_MultipleReadChannels(t *testing.T) { t.Cleanup(metrics.ResetForTest) // Create upstream fake SPDY server which copies STDIN back onto STDOUT and STDERR stream. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Stdin: true, Stdout: true, Stderr: true, @@ -478,7 +479,7 @@ apiserver_stream_translator_requests_total{code="200"} 1 func TestStreamTranslator_ThrottleReadChannels(t *testing.T) { // Create upstream fake SPDY server which copies STDIN back onto STDOUT and STDERR stream. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Stdin: true, Stdout: true, Stderr: true, @@ -621,7 +622,7 @@ func TestStreamTranslator_TTYResizeChannel(t *testing.T) { actualTerminalSizes := make([]remotecommand.TerminalSize, 0, numSizeQueue) // Create upstream fake SPDY server which copies STDIN back onto STDERR stream. spdyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - ctx, err := createSPDYServerStreams(w, req, Options{ + ctx, err := createSPDYServerStreams(t, w, req, Options{ Tty: true, }) if err != nil { @@ -871,9 +872,23 @@ type streamAndReply struct { // connection with remote command streams defined in passed options. Returns a streamContext // structure containing the Reader/Writer streams to communicate through the SDPY connection. // Returns an error if unable to upgrade the HTTP connection to a SPDY connection. -func createSPDYServerStreams(w http.ResponseWriter, req *http.Request, opts Options) (*streamContext, error) { +func createSPDYServerStreams(t *testing.T, w http.ResponseWriter, req *http.Request, opts Options) (*streamContext, error) { _, err := httpstream.Handshake(req, w, []string{rcconstants.StreamProtocolV4Name}) if err != nil { + t.Log("------------------- [DEBUG] HANDSHAKE FAILED -------------------") + t.Logf("Method: %s", req.Method) + t.Logf("URL: %s", req.URL.String()) + t.Logf("Proto: %s", req.Proto) + t.Logf("Header Count: %d", len(req.Header)) + t.Logf("ContentLength: %d", req.ContentLength) // Should be zero for upgrade request + // Dump the request in its entirety. + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Logf("DumpRequest failed: %v", err) + } else { + t.Logf("Full HTTP Request Dump:\n%s", dump) + } + return nil, err } @@ -956,20 +971,11 @@ func v4WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) err } } -func fakeTransport() (*http.Transport, error) { +func fakeTransport() (http.RoundTripper, error) { cfg := &transport.Config{ TLS: transport.TLSConfig{ Insecure: true, - CAFile: "", }, } - rt, err := transport.New(cfg) - if err != nil { - return nil, err - } - t, ok := rt.(*http.Transport) - if !ok { - return nil, fmt.Errorf("unknown transport type: %T", rt) - } - return t, nil + return transport.New(cfg) } diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel.go index 7a7b92ada..429ab78eb 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel.go @@ -31,14 +31,14 @@ import ( gwebsocket "github.com/gorilla/websocket" - "k8s.io/apimachinery/pkg/util/httpstream" - "k8s.io/apimachinery/pkg/util/httpstream/spdy" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" utilnet "k8s.io/apimachinery/pkg/util/net" constants "k8s.io/apimachinery/pkg/util/portforward" "k8s.io/apiserver/pkg/util/proxy/metrics" "k8s.io/client-go/tools/portforward" "k8s.io/klog/v2" + "k8s.io/streaming/pkg/httpstream" + "k8s.io/streaming/pkg/httpstream/spdy" + "k8s.io/streaming/pkg/httpstream/wsstream" ) // TunnelingHandler is a handler which tunnels SPDY through WebSockets. diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel_test.go index 7cf57f2d3..c979ffbce 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/streamtunnel_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/streamtunnel_test.go @@ -33,8 +33,6 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/httpstream" - "k8s.io/apimachinery/pkg/util/httpstream/spdy" constants "k8s.io/apimachinery/pkg/util/portforward" "k8s.io/apimachinery/pkg/util/proxy" "k8s.io/apimachinery/pkg/util/wait" @@ -44,6 +42,8 @@ import ( "k8s.io/client-go/tools/portforward" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" + "k8s.io/streaming/pkg/httpstream" + "k8s.io/streaming/pkg/httpstream/spdy" ) func TestTunnelingHandler_UpgradeStreamingAndTunneling(t *testing.T) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/translatinghandler.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/translatinghandler.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/translatinghandler.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/translatinghandler.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/translatinghandler_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/translatinghandler_test.go similarity index 98% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/translatinghandler_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/translatinghandler_test.go index ee5a53ed8..c4aef1f70 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/translatinghandler_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/translatinghandler_test.go @@ -21,7 +21,7 @@ import ( "testing" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" + "k8s.io/streaming/pkg/httpstream/wsstream" ) // fakeHandler implements http.Handler interface diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/websocket.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/websocket.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/websocket.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/websocket.go index 798ce1767..837c46810 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/proxy/websocket.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/proxy/websocket.go @@ -25,10 +25,10 @@ import ( "time" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/httpstream/wsstream" constants "k8s.io/apimachinery/pkg/util/remotecommand" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/tools/remotecommand" + "k8s.io/streaming/pkg/httpstream/wsstream" ) const ( diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/responsewriter/inmemoryresponsewriter.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/responsewriter/inmemoryresponsewriter.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/responsewriter/inmemoryresponsewriter.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/responsewriter/inmemoryresponsewriter.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/responsewriter/inmemoryresponsewriter_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/responsewriter/inmemoryresponsewriter_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/responsewriter/inmemoryresponsewriter_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/responsewriter/inmemoryresponsewriter_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/shufflesharding/shufflesharding.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/shufflesharding/shufflesharding.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/shufflesharding/shufflesharding.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/shufflesharding/shufflesharding.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/shufflesharding/shufflesharding_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/shufflesharding/shufflesharding_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/shufflesharding/shufflesharding_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/shufflesharding/shufflesharding_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/authentication.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/authentication.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/authentication.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/authentication.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/authentication_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/authentication_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/authentication_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/authentication_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/certs_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/certs_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/certs_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/certs_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/client.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/client.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/client.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/client.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/client_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/client_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/client_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/client_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/error.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/error.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/error.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/error.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/gencerts.sh b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/gencerts.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/gencerts.sh rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/gencerts.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/serviceresolver.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/serviceresolver.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/serviceresolver.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/serviceresolver.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/serviceresolver_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/serviceresolver_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/serviceresolver_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/serviceresolver_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/validation.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/validation.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/validation.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/validation.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/validation_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/validation_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/validation_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/validation_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook.go index b03640ae8..8552e91eb 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook.go @@ -83,6 +83,7 @@ func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFact clientConfig := rest.CopyConfig(config) codec := codecFactory.LegacyCodec(groupVersions...) + clientConfig.ContentType = runtime.ContentTypeJSON clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec}) clientConfig.Wrap(x509metrics.NewDeprecatedCertificateRoundTripperWrapperConstructor( diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook_test.go similarity index 93% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook_test.go index 068c6821e..cfa09aa70 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/webhook/webhook_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/webhook/webhook_test.go @@ -23,6 +23,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net" "net/http" "net/http/httptest" @@ -33,11 +34,15 @@ import ( "strings" "testing" "time" + "unicode/utf8" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/wait" + exampleinstall "k8s.io/apiserver/pkg/apis/example/install" + examplev1 "k8s.io/apiserver/pkg/apis/example/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" v1 "k8s.io/client-go/tools/clientcmd/api/v1" @@ -867,7 +872,7 @@ func TestWithExponentialBackoffParametersNotSet(t *testing.T) { err := WithExponentialBackoff(context.TODO(), wait.Backoff{}, webhookFunc, alwaysRetry) - errExpected := fmt.Errorf("webhook call failed: %s", wait.ErrWaitTimeout) + errExpected := fmt.Errorf("webhook call failed: %s", wait.ErrorInterrupted(nil).Error()) if errExpected.Error() != err.Error() { t.Errorf("expected error: %v, but got: %v", errExpected, err) } @@ -927,3 +932,57 @@ func getSingleCounterValueFromRegistry(t *testing.T, r metrics.Gatherer, name st return -1 } + +func TestRESTConfigContentType(t *testing.T) { + server, err := newTestServer(clientCert, clientKey, caCert, func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Content-Type"); got != runtime.ContentTypeJSON { + t.Errorf("expected request content-type: want %q got %q", runtime.ContentTypeJSON, got) + } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("failed to read request body: %v", err) + return + } + if err := json.Unmarshal(body, new(any)); err != nil { + switch { + case len(body) == 0: + t.Log("empty request body") + case utf8.Valid(body): + t.Logf("request body: %s", string(body)) + default: + t.Logf("request body: 0x%x", body) + } + t.Errorf("failed to unmarshal request body as json: %v", err) + } + }) + if err != nil { + t.Errorf("failed to create server: %v", err) + return + } + defer server.Close() + + config := &rest.Config{ + ContentConfig: rest.ContentConfig{ + ContentType: "foo/bar", + }, + Host: server.URL, + TLSClientConfig: rest.TLSClientConfig{ + CAData: caCert, + CertData: clientCert, + KeyData: clientKey, + }, + } + + scheme := runtime.NewScheme() + exampleinstall.Install(scheme) + codecs := serializer.NewCodecFactory(scheme) + groupVersions := []schema.GroupVersion{examplev1.SchemeGroupVersion} + wh, err := NewGenericWebhook(scheme, codecs, config, groupVersions, retryBackoff) + if err != nil { + t.Fatalf("failed to create the webhook: %v", err) + } + + if err := wh.RestClient.Post().Body(&examplev1.Pod{}).Do(context.TODO()).Error(); err != nil { + t.Fatalf("failed to complete request: %v", err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/x509metrics/server_cert_deprecations.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/x509metrics/server_cert_deprecations.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/x509metrics/server_cert_deprecations.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/x509metrics/server_cert_deprecations.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/util/x509metrics/server_cert_deprecations_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/util/x509metrics/server_cert_deprecations_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/util/x509metrics/server_cert_deprecations_test.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/util/x509metrics/server_cert_deprecations_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics.go similarity index 51% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics.go index 256c23c4b..4d06bc96d 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/pkg/validation/metrics.go +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics.go @@ -28,8 +28,9 @@ const ( // ValidationMetrics is the interface for validation metrics. type ValidationMetrics interface { - IncDeclarativeValidationMismatchMetric() - IncDeclarativeValidationPanicMetric() + IncDeclarativeValidationMismatchMetric(validationIdentifier string) + IncDeclarativeValidationPanicMetric(validationIdentifier string) + IncDuplicateValidationErrorMetric() Reset() } @@ -43,6 +44,16 @@ var validationMetricsInstance = &validationMetrics{ StabilityLevel: metrics.BETA, }, ), + DeclarativeValidationMismatchCounterVector: metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "declarative_validation_parity_discrepancies_total", + Help: "Number of discrepancies between declarative and handwritten validation, broken down by validation identifier.", + StabilityLevel: metrics.ALPHA, + }, + []string{"validation_identifier"}, + ), DeclarativeValidationPanicCounter: metrics.NewCounter( &metrics.CounterOpts{ Namespace: namespace, @@ -52,6 +63,25 @@ var validationMetricsInstance = &validationMetrics{ StabilityLevel: metrics.BETA, }, ), + DeclarativeValidationPanicCounterVector: metrics.NewCounterVec( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "declarative_validation_panics_total", + Help: "Number of panics in declarative validation, broken down by validation identifier.", + StabilityLevel: metrics.ALPHA, + }, + []string{"validation_identifier"}, + ), + DuplicateValidationErrorCounter: metrics.NewCounter( + &metrics.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "duplicate_validation_error_total", + Help: "Number of duplicate validation errors during validation.", + StabilityLevel: metrics.INTERNAL, + }, + ), } // Metrics provides access to validation metrics. @@ -59,28 +89,44 @@ var Metrics ValidationMetrics = validationMetricsInstance func init() { legacyregistry.MustRegister(validationMetricsInstance.DeclarativeValidationMismatchCounter) + legacyregistry.MustRegister(validationMetricsInstance.DeclarativeValidationMismatchCounterVector) legacyregistry.MustRegister(validationMetricsInstance.DeclarativeValidationPanicCounter) + legacyregistry.MustRegister(validationMetricsInstance.DeclarativeValidationPanicCounterVector) + legacyregistry.MustRegister(validationMetricsInstance.DuplicateValidationErrorCounter) } type validationMetrics struct { - DeclarativeValidationMismatchCounter *metrics.Counter - DeclarativeValidationPanicCounter *metrics.Counter + DeclarativeValidationMismatchCounter *metrics.Counter + DeclarativeValidationMismatchCounterVector *metrics.CounterVec + DeclarativeValidationPanicCounter *metrics.Counter + DeclarativeValidationPanicCounterVector *metrics.CounterVec + DuplicateValidationErrorCounter *metrics.Counter } // Reset resets the validation metrics. func (m *validationMetrics) Reset() { m.DeclarativeValidationMismatchCounter.Reset() + m.DeclarativeValidationMismatchCounterVector.Reset() m.DeclarativeValidationPanicCounter.Reset() + m.DeclarativeValidationPanicCounterVector.Reset() + m.DuplicateValidationErrorCounter.Reset() } // IncDeclarativeValidationMismatchMetric increments the counter for the declarative_validation_mismatch_total metric. -func (m *validationMetrics) IncDeclarativeValidationMismatchMetric() { +func (m *validationMetrics) IncDeclarativeValidationMismatchMetric(validationIdentifier string) { m.DeclarativeValidationMismatchCounter.Inc() + m.DeclarativeValidationMismatchCounterVector.WithLabelValues(validationIdentifier).Inc() } // IncDeclarativeValidationPanicMetric increments the counter for the declarative_validation_panic_total metric. -func (m *validationMetrics) IncDeclarativeValidationPanicMetric() { +func (m *validationMetrics) IncDeclarativeValidationPanicMetric(validationIdentifier string) { m.DeclarativeValidationPanicCounter.Inc() + m.DeclarativeValidationPanicCounterVector.WithLabelValues(validationIdentifier).Inc() +} + +// IncDuplicateValidationErrorMetric increments the counter for the duplicate_validation_error_total metric. +func (m *validationMetrics) IncDuplicateValidationErrorMetric() { + m.DuplicateValidationErrorCounter.Inc() } func ResetValidationMetricsInstance() { diff --git a/third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics_test.go new file mode 100644 index 000000000..016a6956e --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/pkg/validation/metrics_test.go @@ -0,0 +1,231 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validation + +import ( + "strings" + "testing" + + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/component-base/metrics/testutil" +) + +const testIdentifier = "test_validation_identifier" +const anotherTestIdentifier = "another_test_validation_identifier" + +// TestDeclarativeValidationMismatchMetric tests that the mismatch metric correctly increments once +func TestDeclarativeValidationMismatchMetric(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric once + Metrics.IncDeclarativeValidationMismatchMetric(testIdentifier) + + expected := ` + # HELP apiserver_validation_declarative_validation_parity_discrepancies_total [ALPHA] Number of discrepancies between declarative and handwritten validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_parity_discrepancies_total counter + apiserver_validation_declarative_validation_parity_discrepancies_total{validation_identifier="test_validation_identifier"} 1 + # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. + # TYPE apiserver_validation_declarative_validation_mismatch_total counter + apiserver_validation_declarative_validation_mismatch_total 1 + + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_validation_declarative_validation_parity_discrepancies_total", "apiserver_validation_declarative_validation_mismatch_total"); err != nil { + t.Fatal(err) + } +} + +// TestDeclarativeValidationPanicMetric tests that the panic metric correctly increments once +func TestDeclarativeValidationPanicMetric(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric once + Metrics.IncDeclarativeValidationPanicMetric(testIdentifier) + + expected := ` + # HELP apiserver_validation_declarative_validation_panics_total [ALPHA] Number of panics in declarative validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_panics_total counter + apiserver_validation_declarative_validation_panics_total{validation_identifier="test_validation_identifier"} 1 + # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. + # TYPE apiserver_validation_declarative_validation_panic_total counter + apiserver_validation_declarative_validation_panic_total 1 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), + "apiserver_validation_declarative_validation_panic_total", "apiserver_validation_declarative_validation_panics_total"); err != nil { + t.Fatal(err) + } +} + +// TestDuplicateValidationErrorMetric tests that the duplicate error metric correctly increments once +func TestDuplicateValidationErrorMetric(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric once + Metrics.IncDuplicateValidationErrorMetric() + + expected := ` + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 1 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_validation_duplicate_validation_error_total"); err != nil { + t.Fatal(err) + } +} + +// TestDeclarativeValidationMismatchMetricMultiple tests that the mismatch metric correctly increments multiple times +func TestDeclarativeValidationMismatchMetricMultiple(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric three times + Metrics.IncDeclarativeValidationMismatchMetric(testIdentifier) + Metrics.IncDeclarativeValidationMismatchMetric(testIdentifier) + Metrics.IncDeclarativeValidationMismatchMetric(anotherTestIdentifier) + + expected := ` + # HELP apiserver_validation_declarative_validation_parity_discrepancies_total [ALPHA] Number of discrepancies between declarative and handwritten validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_parity_discrepancies_total counter + apiserver_validation_declarative_validation_parity_discrepancies_total{validation_identifier="test_validation_identifier"} 2 + apiserver_validation_declarative_validation_parity_discrepancies_total{validation_identifier="another_test_validation_identifier"} 1 + # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. + # TYPE apiserver_validation_declarative_validation_mismatch_total counter + apiserver_validation_declarative_validation_mismatch_total 3 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_validation_declarative_validation_parity_discrepancies_total", "apiserver_validation_declarative_validation_mismatch_total"); err != nil { + t.Fatal(err) + } +} + +// TestDeclarativeValidationPanicMetricMultiple tests that the panic metric correctly increments multiple times +func TestDeclarativeValidationPanicMetricMultiple(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric three times + Metrics.IncDeclarativeValidationPanicMetric(testIdentifier) + Metrics.IncDeclarativeValidationPanicMetric(testIdentifier) + Metrics.IncDeclarativeValidationPanicMetric(anotherTestIdentifier) + + expected := ` + # HELP apiserver_validation_declarative_validation_panics_total [ALPHA] Number of panics in declarative validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_panics_total counter + apiserver_validation_declarative_validation_panics_total{validation_identifier="test_validation_identifier"} 2 + apiserver_validation_declarative_validation_panics_total{validation_identifier="another_test_validation_identifier"} 1 + # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. + # TYPE apiserver_validation_declarative_validation_panic_total counter + apiserver_validation_declarative_validation_panic_total 3 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_validation_declarative_validation_panic_total", "apiserver_validation_declarative_validation_panics_total"); err != nil { + t.Fatal(err) + } +} + +// TestDuplicateValidationErrorMetricMultiple tests that the duplicate error metric correctly increments multiple times +func TestDuplicateValidationErrorMetricMultiple(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment the metric three times + Metrics.IncDuplicateValidationErrorMetric() + Metrics.IncDuplicateValidationErrorMetric() + Metrics.IncDuplicateValidationErrorMetric() + + expected := ` + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 3 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), "apiserver_validation_duplicate_validation_error_total"); err != nil { + t.Fatal(err) + } +} + +// TestDeclarativeValidationMetricsReset tests that the Reset function correctly resets the metrics to zero +func TestDeclarativeValidationMetricsReset(t *testing.T) { + defer legacyregistry.Reset() + defer ResetValidationMetricsInstance() + + // Increment both metrics + Metrics.IncDeclarativeValidationMismatchMetric(testIdentifier) + Metrics.IncDeclarativeValidationPanicMetric(testIdentifier) + Metrics.IncDuplicateValidationErrorMetric() + + // Reset the metrics + Metrics.Reset() + + // Verify they've been reset to zero + expected := ` + # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. + # TYPE apiserver_validation_declarative_validation_mismatch_total counter + apiserver_validation_declarative_validation_mismatch_total 0 + # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. + # TYPE apiserver_validation_declarative_validation_panic_total counter + apiserver_validation_declarative_validation_panic_total 0 + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 0 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), + "apiserver_validation_declarative_validation_mismatch_total", + "apiserver_validation_declarative_validation_panic_total", + "apiserver_validation_duplicate_validation_error_total"); err != nil { + t.Fatal(err) + } + + // Increment the metrics again to ensure they're still functional + Metrics.IncDeclarativeValidationMismatchMetric(testIdentifier) + Metrics.IncDeclarativeValidationPanicMetric(testIdentifier) + Metrics.IncDuplicateValidationErrorMetric() + + // Verify they've been incremented correctly + expected = ` + # HELP apiserver_validation_declarative_validation_mismatch_total [BETA] Number of times declarative validation results differed from handwritten validation results for core types. + # TYPE apiserver_validation_declarative_validation_mismatch_total counter + apiserver_validation_declarative_validation_mismatch_total 1 + # HELP apiserver_validation_declarative_validation_panic_total [BETA] Number of times declarative validation has panicked during validation. + # TYPE apiserver_validation_declarative_validation_panic_total counter + apiserver_validation_declarative_validation_panic_total 1 + # HELP apiserver_validation_duplicate_validation_error_total [INTERNAL] Number of duplicate validation errors during validation. + # TYPE apiserver_validation_duplicate_validation_error_total counter + apiserver_validation_duplicate_validation_error_total 1 + # HELP apiserver_validation_declarative_validation_parity_discrepancies_total [ALPHA] Number of discrepancies between declarative and handwritten validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_parity_discrepancies_total counter + apiserver_validation_declarative_validation_parity_discrepancies_total{validation_identifier="test_validation_identifier"} 1 + # HELP apiserver_validation_declarative_validation_panics_total [ALPHA] Number of panics in declarative validation, broken down by validation identifier. + # TYPE apiserver_validation_declarative_validation_panics_total counter + apiserver_validation_declarative_validation_panics_total{validation_identifier="test_validation_identifier"} 1 + ` + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expected), + "apiserver_validation_declarative_validation_mismatch_total", + "apiserver_validation_declarative_validation_parity_discrepancies_total", + "apiserver_validation_declarative_validation_panic_total", + "apiserver_validation_declarative_validation_panics_total", + "apiserver_validation_duplicate_validation_error_total"); err != nil { + t.Fatal(err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/pkg/warning/context.go b/third_party/k8s.io/apiserver-v0.36.1/pkg/warning/context.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/pkg/warning/context.go rename to third_party/k8s.io/apiserver-v0.36.1/pkg/warning/context.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/buffered.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/buffered.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/buffered.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/buffered.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/buffered_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/buffered_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/buffered_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/buffered_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/doc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/buffered/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/buffered/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/doc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/fake/doc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/fake/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/fake/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/fake/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/fake/fake.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/fake/fake.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/fake/fake.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/fake/fake.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/log/backend.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/log/backend.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/log/backend.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/log/backend.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/log/backend_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/log/backend_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/log/backend_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/log/backend_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/doc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/doc.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/truncate.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/truncate.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/truncate.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/truncate.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/truncate_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/truncate_test.go similarity index 99% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/truncate_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/truncate_test.go index a8ae1582d..644f81fde 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/truncate/truncate_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/truncate/truncate_test.go @@ -70,7 +70,6 @@ func TestTruncatingEvents(t *testing.T) { } for _, tc := range testCases { - tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() @@ -122,7 +121,6 @@ func TestSplittingBatches(t *testing.T) { }, } for _, tc := range testCases { - tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/webhook/webhook.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/webhook/webhook.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/webhook/webhook.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/webhook/webhook.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/webhook/webhook_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/webhook/webhook_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/audit/webhook/webhook_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/audit/webhook/webhook_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/doc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/doc.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/doc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/doc.go diff --git a/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics.go new file mode 100644 index 000000000..8c72ac681 --- /dev/null +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics.go @@ -0,0 +1,249 @@ +/* +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 oidc + +import ( + "context" + "crypto/sha256" + "fmt" + "sync" + "time" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/component-base/metrics" + "k8s.io/component-base/metrics/legacyregistry" + "k8s.io/utils/clock" +) + +const ( + namespace = "apiserver" + subsystem = "authentication" +) + +var ( + jwtAuthenticatorLatencyMetric = metrics.NewHistogramVec( + &metrics.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "jwt_authenticator_latency_seconds", + Help: "Latency of jwt authentication operations in seconds. This is the time spent authenticating a token for cache miss only (i.e. when the token is not found in the cache).", + StabilityLevel: metrics.ALPHA, + // default histogram buckets with a 1ms starting point + Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, + }, + []string{"result", "jwt_issuer_hash"}, + ) + + jwksFetchLastTimestampSeconds = metrics.NewGaugeVec( + &metrics.GaugeOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "jwt_authenticator_jwks_fetch_last_timestamp_seconds", + Help: "Timestamp of the last successful or failed JWKS fetch split by result, api server identity " + + "and jwt issuer for the JWT authenticator.", + StabilityLevel: metrics.ALPHA, + }, + []string{"result", "jwt_issuer_hash", "apiserver_id_hash"}, + ) + + jwksFetchLastKeySetInfo = metrics.NewDesc( + metrics.BuildFQName(namespace, subsystem, "jwt_authenticator_jwks_fetch_last_key_set_info"), + "Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer.", + []string{"jwt_issuer_hash", "apiserver_id_hash", "hash"}, + nil, + metrics.ALPHA, + "", + ) +) + +// jwksHashKey uniquely identifies a JWKS by issuer and API server ID +type jwksHashKey struct { + jwtIssuerHash string + apiServerIDHash string +} + +// jwksHashProvider manages JWKS hashes for all authenticators +type jwksHashProvider struct { + hashes sync.Map // map[jwksHashKey]string +} + +func newJWKSHashProvider() *jwksHashProvider { + return &jwksHashProvider{} +} + +func (p *jwksHashProvider) setHash(jwtIssuer, apiServerID, keySet string) { + key := jwksHashKey{ + jwtIssuerHash: getHash(jwtIssuer), + apiServerIDHash: getHash(apiServerID), + } + jwksHash := getHash(keySet) + p.hashes.Store(key, jwksHash) +} + +func (p *jwksHashProvider) getHashes() map[jwksHashKey]string { + result := make(map[jwksHashKey]string) + p.hashes.Range(func(k, v interface{}) bool { + result[k.(jwksHashKey)] = v.(string) + return true + }) + return result +} + +func (p *jwksHashProvider) reset() { + p.hashes.Range(func(k, v interface{}) bool { + p.hashes.Delete(k) + return true + }) +} + +func (p *jwksHashProvider) deleteHash(jwtIssuer, apiServerID string) { + key := jwksHashKey{ + jwtIssuerHash: getHash(jwtIssuer), + apiServerIDHash: getHash(apiServerID), + } + p.hashes.Delete(key) +} + +// jwksHashCollector is a custom collector that emits JWKS hash metrics +type jwksHashCollector struct { + metrics.BaseStableCollector + desc *metrics.Desc + hashProvider *jwksHashProvider +} + +func newJWKSHashCollector(desc *metrics.Desc, hashProvider *jwksHashProvider) metrics.StableCollector { + return &jwksHashCollector{ + desc: desc, + hashProvider: hashProvider, + } +} + +func (c *jwksHashCollector) DescribeWithStability(ch chan<- *metrics.Desc) { + ch <- c.desc +} + +func (c *jwksHashCollector) CollectWithStability(ch chan<- metrics.Metric) { + hashes := c.hashProvider.getHashes() + for key, hash := range hashes { + ch <- metrics.NewLazyConstMetric( + c.desc, + metrics.GaugeValue, + 1, + key.jwtIssuerHash, + key.apiServerIDHash, + hash, + ) + } +} + +var registerMetrics sync.Once +var hashProvider = newJWKSHashProvider() + +func RegisterMetrics() { + registerMetrics.Do(func() { + legacyregistry.MustRegister(jwtAuthenticatorLatencyMetric) + legacyregistry.MustRegister(jwksFetchLastTimestampSeconds) + legacyregistry.CustomMustRegister(newJWKSHashCollector(jwksFetchLastKeySetInfo, hashProvider)) + }) +} + +func recordAuthenticationLatency(result, jwtIssuerHash string, duration time.Duration) { + jwtAuthenticatorLatencyMetric.WithLabelValues(result, jwtIssuerHash).Observe(duration.Seconds()) +} + +func recordJWKSFetchTimestamp(result, jwtIssuer, apiServerID string) { + jwksFetchLastTimestampSeconds.WithLabelValues(result, getHash(jwtIssuer), getHash(apiServerID)).SetToCurrentTime() +} + +func recordJWKSFetchKeySetSuccess(jwtIssuer, apiServerID, keySet string) { + recordJWKSFetchKeySetHash(jwtIssuer, apiServerID, keySet) + recordJWKSFetchTimestamp("success", jwtIssuer, apiServerID) +} + +func recordJWKSFetchKeySetFailure(jwtIssuer, apiServerID string) { + recordJWKSFetchTimestamp("failure", jwtIssuer, apiServerID) +} + +func recordJWKSFetchKeySetHash(jwtIssuer, apiServerID, keySet string) { + hashProvider.setHash(jwtIssuer, apiServerID, keySet) +} + +// DeleteJWKSFetchMetrics deletes all JWKS-related metrics for a specific issuer and API server. +// This includes the hash metric and timestamp metrics (both success and failure). +// This should be called when an issuer is removed from the configuration to clean up stale metrics. +func DeleteJWKSFetchMetrics(jwtIssuer, apiServerID string) { + jwtIssuerHash := getHash(jwtIssuer) + apiServerIDHash := getHash(apiServerID) + + hashProvider.deleteHash(jwtIssuer, apiServerID) + + jwksFetchLastTimestampSeconds.DeleteLabelValues("success", jwtIssuerHash, apiServerIDHash) + jwksFetchLastTimestampSeconds.DeleteLabelValues("failure", jwtIssuerHash, apiServerIDHash) +} + +func ResetMetrics() { + jwtAuthenticatorLatencyMetric.Reset() + jwksFetchLastTimestampSeconds.Reset() + hashProvider.reset() +} + +func getHash(data string) string { + if len(data) > 0 { + return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(data))) + } + return "" +} + +func newInstrumentedAuthenticator(jwtIssuer string, delegate AuthenticatorTokenWithHealthCheck) AuthenticatorTokenWithHealthCheck { + return newInstrumentedAuthenticatorWithClock(jwtIssuer, delegate, clock.RealClock{}) +} + +func newInstrumentedAuthenticatorWithClock(jwtIssuer string, delegate AuthenticatorTokenWithHealthCheck, clock clock.PassiveClock) *instrumentedAuthenticator { + return &instrumentedAuthenticator{ + jwtIssuerHash: getHash(jwtIssuer), + delegate: delegate, + clock: clock, + } +} + +type instrumentedAuthenticator struct { + jwtIssuerHash string + delegate AuthenticatorTokenWithHealthCheck + clock clock.PassiveClock +} + +func (a *instrumentedAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { + start := a.clock.Now() + response, ok, err := a.delegate.AuthenticateToken(ctx, token) + // this only happens when issuer doesn't match the authenticator + // we don't want to record metrics for this case + if !ok && err == nil { + return response, ok, err + } + + duration := a.clock.Since(start) + if err != nil { + recordAuthenticationLatency("failure", a.jwtIssuerHash, duration) + } else { + recordAuthenticationLatency("success", a.jwtIssuerHash, duration) + } + return response, ok, err +} + +func (a *instrumentedAuthenticator) HealthCheck() error { + return a.delegate.HealthCheck() +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics_test.go similarity index 59% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics_test.go index cdcad1d05..0628a3dee 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/metrics_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/metrics_test.go @@ -29,7 +29,9 @@ import ( ) const ( - testIssuer = "testIssuer" + testIssuer = "testIssuer" + testAPIServerID = "testAPIServerID" + testKeySet = `{"keys":[{"kty":"RSA","use":"sig","kid":"test"}]}` ) func TestRecordAuthenticationLatency(t *testing.T) { @@ -131,3 +133,125 @@ func (d dummyClock) Now() time.Time { func (d dummyClock) Since(t time.Time) time.Duration { return time.Duration(1) } + +func TestRecordJWKSFetchKeySetSuccess(t *testing.T) { + expectedValue := ` + # HELP apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info [ALPHA] Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer. + # TYPE apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info gauge + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="sha256:d132d414ef2da3d863abd7bf0165c00403ef1d3510faf8fdf1d7cf335c888e53",jwt_issuer_hash="sha256:29b34beedc55b972f2428f21bc588f9d38e5e8f7a7af825486e7bb4fd9caa2ad"} 1 + ` + + metrics := []string{ + namespace + "_" + subsystem + "_jwt_authenticator_jwks_fetch_last_key_set_info", + } + + ResetMetrics() + RegisterMetrics() + + recordJWKSFetchKeySetSuccess(testIssuer, testAPIServerID, testKeySet) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } +} + +func TestRecordJWKSFetchKeySetFailure(t *testing.T) { + ResetMetrics() + RegisterMetrics() + + recordJWKSFetchKeySetFailure(testIssuer, testAPIServerID) + + metrics, err := legacyregistry.DefaultGatherer.Gather() + if err != nil { + t.Fatal(err) + } + + found := false + for _, m := range metrics { + if m.GetName() == namespace+"_"+subsystem+"_jwt_authenticator_jwks_fetch_last_timestamp_seconds" { + found = true + break + } + } + if !found { + t.Fatal("Expected jwt_authenticator_jwks_fetch_last_timestamp_seconds metric to be present") + } +} + +func TestJWKSHashCollector_MultipleAuthenticators(t *testing.T) { + expectedValue := ` + # HELP apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info [ALPHA] Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer. + # TYPE apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info gauge + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="sha256:d132d414ef2da3d863abd7bf0165c00403ef1d3510faf8fdf1d7cf335c888e53",jwt_issuer_hash="sha256:29b34beedc55b972f2428f21bc588f9d38e5e8f7a7af825486e7bb4fd9caa2ad"} 1 + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="sha256:1b5293c65ffc96e13f2d6fefae782190aec8cfb89957a3109419d4f47b80e3e8",jwt_issuer_hash="sha256:f10ab1bafaa1a8628d0fae41ee554948912b01957e4a2db1698fc1c3e4451682"} 1 + ` + + metrics := []string{ + namespace + "_" + subsystem + "_jwt_authenticator_jwks_fetch_last_key_set_info", + } + + ResetMetrics() + RegisterMetrics() + + recordJWKSFetchKeySetSuccess(testIssuer, testAPIServerID, testKeySet) + + secondIssuer := "https://another-issuer.example.com" + secondKeySet := `{"keys":[{"kty":"EC","use":"sig","kid":"test2"}]}` + recordJWKSFetchKeySetSuccess(secondIssuer, testAPIServerID, secondKeySet) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } +} + +func TestJWKSHashCollector_UpdateExistingHash(t *testing.T) { + expectedValue := ` + # HELP apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info [ALPHA] Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer. + # TYPE apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info gauge + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="sha256:1b5293c65ffc96e13f2d6fefae782190aec8cfb89957a3109419d4f47b80e3e8",jwt_issuer_hash="sha256:29b34beedc55b972f2428f21bc588f9d38e5e8f7a7af825486e7bb4fd9caa2ad"} 1 + ` + + metrics := []string{ + namespace + "_" + subsystem + "_jwt_authenticator_jwks_fetch_last_key_set_info", + } + + ResetMetrics() + RegisterMetrics() + + recordJWKSFetchKeySetSuccess(testIssuer, testAPIServerID, testKeySet) + + // Update with new JWKS - should replace old hash with new one + updatedKeySet := `{"keys":[{"kty":"EC","use":"sig","kid":"test2"}]}` + recordJWKSFetchKeySetSuccess(testIssuer, testAPIServerID, updatedKeySet) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } +} + +func TestJWKSHashCollector_DeleteHash(t *testing.T) { + ResetMetrics() + RegisterMetrics() + + recordJWKSFetchKeySetSuccess(testIssuer, testAPIServerID, testKeySet) + secondIssuer := "https://another-issuer.example.com" + secondKeySet := `{"keys":[{"kty":"EC","use":"sig","kid":"test2"}]}` + recordJWKSFetchKeySetSuccess(secondIssuer, testAPIServerID, secondKeySet) + + // Delete first authenticator's metrics and verify only second authenticator's hash remains + DeleteJWKSFetchMetrics(testIssuer, testAPIServerID) + + expectedValue := ` + # HELP apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info [ALPHA] Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer. + # TYPE apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info gauge + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="sha256:1b5293c65ffc96e13f2d6fefae782190aec8cfb89957a3109419d4f47b80e3e8",jwt_issuer_hash="sha256:f10ab1bafaa1a8628d0fae41ee554948912b01957e4a2db1698fc1c3e4451682"} 1 + ` + + metrics := []string{ + namespace + "_" + subsystem + "_jwt_authenticator_jwks_fetch_last_key_set_info", + } + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedValue), metrics...); err != nil { + t.Fatal(err) + } +} diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc.go similarity index 89% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc.go index d9063fcd1..688f7f6ee 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc.go @@ -27,6 +27,7 @@ oidc implements the authenticator.Token interface using the OpenID Connect proto package oidc import ( + "bytes" "context" "crypto/tls" "crypto/x509" @@ -34,6 +35,7 @@ import ( "encoding/json" "fmt" "io" + "mime" "net/http" "net/url" "strings" @@ -46,6 +48,7 @@ import ( "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" + jose "gopkg.in/go-jose/go-jose.v2" "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/sets" @@ -58,7 +61,9 @@ import ( "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/lazy" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/egressselector" + utilfeature "k8s.io/apiserver/pkg/util/feature" certutil "k8s.io/client-go/util/cert" "k8s.io/klog/v2" ) @@ -79,6 +84,10 @@ type Options struct { // Optional KeySet to allow for synchronous initialization instead of fetching from the remote issuer. // Mutually exclusive with JWTAuthenticator.Issuer.DiscoveryURL. + // + // The following API server metrics for fetching JWKS and provider status will not be recorded if this is set. + // - apiserver_authentication_jwt_authenticator_jwks_fetch_last_timestamp_seconds + // - apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info KeySet oidc.KeySet // PEM encoded root certificate contents of the provider. Mutually exclusive with Client. @@ -109,6 +118,10 @@ type Options struct { DisallowedIssuers []string + // APIServerID is the ID of the API server + // This is used in metrics to identify the API server + APIServerID string + // now is used for testing. It defaults to time.Now. now func() time.Time } @@ -264,6 +277,7 @@ func New(lifecycleCtx context.Context, opts Options) (AuthenticatorTokenWithHeal return nil, err } + RegisterMetrics() supportedSigningAlgs := opts.SupportedSigningAlgs if len(supportedSigningAlgs) == 0 { // RS256 is the default recommended by OpenID Connect and an 'alg' value @@ -421,6 +435,31 @@ func New(lifecycleCtx context.Context, opts Options) (AuthenticatorTokenWithHeal return false, nil } + if utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfigurationJWKSMetrics) { + providerJSON := &struct { + JWKSURL string `json:"jwks_uri"` + }{} + + if err := provider.Claims(providerJSON); err != nil { + klog.Errorf("oidc authenticator: error getting JWKS URL: %v", err) + authn.healthCheck.Store(&errorHolder{err: err}) + return false, nil + } + if len(providerJSON.JWKSURL) == 0 { + klog.Errorf("provider did not return JWKS URL") + authn.healthCheck.Store(&errorHolder{err: fmt.Errorf("provider did not return JWKS URL")}) + return false, nil + } + + clientWithJWKSMetrics := *client + clientWithJWKSMetrics.Transport = withMetricsRoundTripper(client.Transport, providerJSON.JWKSURL, issuerURL, opts.APIServerID, lifecycleCtx) + client = &clientWithJWKSMetrics + + remoteKeySet := oidc.NewRemoteKeySet(oidc.ClientContext(lifecycleCtx, client), providerJSON.JWKSURL) + authn.setVerifier(&idTokenVerifier{oidc.NewVerifier(issuerURL, remoteKeySet, verifierConfig), audiences}) + return true, nil + } + verifier := provider.Verifier(verifierConfig) authn.setVerifier(&idTokenVerifier{verifier, audiences}) return true, nil @@ -451,6 +490,100 @@ type errorHolder struct { err error } +// metricsRoundTripper is a http.RoundTripper that records metrics for JWKS fetches. +type metricsRoundTripper struct { + base http.RoundTripper + jwksURL string + + jwtIssuer string + apiServerID string + shouldRecordFunc func() bool +} + +// shouldRecordMetrics returns true if metrics should be recorded. +// This is used to avoid recording metrics after the lifecycle context is done. +func (m *metricsRoundTripper) shouldRecordMetrics() bool { + return m.shouldRecordFunc() +} + +func (m *metricsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if req.URL.String() != m.jwksURL || !m.shouldRecordMetrics() { + return m.base.RoundTrip(req) + } + + resp, err := m.base.RoundTrip(req) + if err != nil || resp.StatusCode != http.StatusOK || resp.Body == nil { + if m.shouldRecordMetrics() { + recordJWKSFetchKeySetFailure(m.jwtIssuer, m.apiServerID) + } + return resp, err + } + + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + if err != nil { + if m.shouldRecordMetrics() { + recordJWKSFetchKeySetFailure(m.jwtIssuer, m.apiServerID) + } + return resp, fmt.Errorf("error reading JWKS response: %w", err) + } + + // This check has been copied from https://github.com/coreos/go-oidc/blob/0fe98873951208147e6d412602432038c91cda54/oidc/jwks.go#L263-L267 + // to validate the key set before setting the metrics to avoid recording invalid key sets. + var keySet jose.JSONWebKeySet + if m.shouldRecordMetrics() { + if unmarshalErr := unmarshalResp(resp, body, &keySet); unmarshalErr != nil { + recordJWKSFetchKeySetFailure(m.jwtIssuer, m.apiServerID) + } else { + recordJWKSFetchKeySetSuccess(m.jwtIssuer, m.apiServerID, string(body)) + } + } + + newResp := &http.Response{} + *newResp = *resp + newResp.Body = io.NopCloser(bytes.NewBuffer(body)) + + return newResp, nil +} + +// This function is copied from https://github.com/coreos/go-oidc/blob/0fe98873951208147e6d412602432038c91cda54/oidc/oidc.go#L543-L554 +// to validate the key set before setting the metrics to avoid recording invalid key sets. +func unmarshalResp(r *http.Response, body []byte, v interface{}) error { + err := json.Unmarshal(body, &v) + if err == nil { + return nil + } + ct := r.Header.Get("Content-Type") + mediaType, _, parseErr := mime.ParseMediaType(ct) + if parseErr == nil && mediaType == "application/json" { + return fmt.Errorf("got Content-Type = application/json, but could not unmarshal as JSON: %w", err) + } + return fmt.Errorf("expected Content-Type = application/json, got %q: %w", ct, err) +} + +// withMetricsRoundTripper returns a new http.RoundTripper that records metrics for JWKS fetches. +func withMetricsRoundTripper(base http.RoundTripper, jwksURL, jwtIssuer, apiServerID string, lifecycleCtx context.Context) http.RoundTripper { + if base == nil { + base = http.DefaultTransport + } + return &metricsRoundTripper{ + base: base, + jwksURL: jwksURL, + jwtIssuer: jwtIssuer, + apiServerID: apiServerID, + shouldRecordFunc: func() bool { + select { + case <-lifecycleCtx.Done(): + return false + default: + return true + } + }, + } +} + // discoveryURLRoundTripper is a http.RoundTripper that rewrites the // {url}/.well-known/openid-configuration to the discovery URL. type discoveryURLRoundTripper struct { diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc_test.go similarity index 90% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc_test.go index 36bba4d1a..08cd28463 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/oidc_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/oidc_test.go @@ -31,6 +31,7 @@ import ( "os" "reflect" "strings" + "sync" "sync/atomic" "testing" "text/template" @@ -42,8 +43,12 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/server/dynamiccertificates" "k8s.io/apiserver/pkg/server/egressselector" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" "k8s.io/klog/v2" "k8s.io/utils/ptr" @@ -152,6 +157,25 @@ type claimsTest struct { fetchKeysFromRemote bool } +type testClaimsServer struct { + *httptest.Server + + keys jose.JSONWebKeySet + mu sync.RWMutex +} + +func (c *testClaimsServer) setKeys(keys jose.JSONWebKeySet) { + c.mu.Lock() + defer c.mu.Unlock() + c.keys = keys +} + +func (c *testClaimsServer) getKeys() jose.JSONWebKeySet { + c.mu.RLock() + defer c.mu.RUnlock() + return c.keys +} + // Replace formats the contents of v into the provided template. func replace(tmpl string, v interface{}) string { t := template.Must(template.New("test").Parse(tmpl)) @@ -166,19 +190,29 @@ func replace(tmpl string, v interface{}) string { // OIDC responses to requests that resolve distributed claims. signer is the // signer used for the served JWT tokens. claimToResponseMap is a map of // responses that the server will return for each claim it is given. -func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *testClaimsServer { + cs := &testClaimsServer{ + keys: keys, + mu: sync.RWMutex{}, + } + + cs.Server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { klog.V(5).Infof("request: %+v", *r) switch r.URL.Path { case "/.testing/keys": w.Header().Set("Content-Type", "application/json") - keyBytes, err := json.Marshal(keys) + keyBytes, err := json.Marshal(cs.getKeys()) if err != nil { t.Fatalf("unexpected error while marshaling keys: %v", err) } klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes)) w.Write(keyBytes) + case "/.testing/invalidkeys": + w.Header().Set("Content-Type", "application/json") + case "/.testing/keys/badstatus": + w.WriteHeader(http.StatusInternalServerError) + // /c/d/bar/.well-known/openid-configuration is used to test issuer url and discovery url with a path case "/.well-known/openid-configuration", "/c/d/bar/.well-known/openid-configuration": w.Header().Set("Content-Type", "application/json") @@ -211,8 +245,8 @@ func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, c fmt.Fprintf(w, "unexpected URL: %v", r.URL) } })) - klog.V(4).Infof("Serving OIDC at: %v", ts.URL) - return ts + klog.V(4).Infof("Serving OIDC at: %v", cs.Server.URL) + return cs } func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet { @@ -4611,6 +4645,417 @@ func TestUnmarshalClaim(t *testing.T) { } } +func TestJWKSMetricsKeySetError(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfigurationJWKSMetrics, true) + + synchronizeTokenIDVerifierForTest = true + signingKey := loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256) + pubKeys := []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + } + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm), + Key: signingKey, + }, nil) + if err != nil { + t.Fatalf("initialize signer: %v", err) + } + + tests := []struct { + name string + openIDConfig string + }{ + { + name: "invalid keyset", + openIDConfig: `{ + "issuer": "{{.URL}}", + "jwks_uri": "{{.URL}}/.testing/invalidkeys" + }`, + }, + { + name: "bad status code", + openIDConfig: `{ + "issuer": "{{.URL}}", + "jwks_uri": "{{.URL}}/.testing/keys/badstatus" + }`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ResetMetrics() + + ts := newClaimServer(t, toKeySet(pubKeys), signer, nil, &test.openIDConfig) + defer ts.Close() + + v := struct { + URL string + }{ + URL: ts.URL, + } + test.openIDConfig = replace(test.openIDConfig, &v) + + opts := Options{ + JWTAuthenticator: apiserver.JWTAuthenticator{ + Issuer: apiserver.Issuer{ + URL: ts.URL, + Audiences: []string{"my-client"}, + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To(""), + }, + }, + }, + now: func() time.Time { return now }, + APIServerID: "testAPIServerID", + } + + // Make the certificate of the helper server available to the authenticator + caBundle := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: ts.Certificate().Raw, + }) + caContent, err := dynamiccertificates.NewStaticCAContent("oidc-authenticator", caBundle) + if err != nil { + t.Fatalf("initialize ca: %v", err) + } + opts.CAContentProvider = caContent + + ctx := testContext(t) + // Initialize the authenticator. + a, err := New(ctx, opts) + if err != nil { + t.Fatalf("initialize authenticator: %v", err) + } + + claims := fmt.Sprintf(`{ +"iss": "%s", +"aud": "my-client", +"username": "jane", +"exp": %d +}`, ts.URL, valid.Unix()) + + jws, err := signer.Sign([]byte(claims)) + if err != nil { + t.Fatalf("sign claims: %v", err) + } + token, err := jws.CompactSerialize() + if err != nil { + t.Fatalf("serialize token: %v", err) + } + + ia, ok := a.(*instrumentedAuthenticator) + if !ok { + t.Fatalf("expected authenticator to be instrumented") + } + authenticator, ok := ia.delegate.(*jwtAuthenticator) + if !ok { + t.Fatalf("expected delegate to be Authenticator") + } + // wait for the authenticator to be initialized + err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) { + if v, _ := authenticator.idTokenVerifier(); v == nil { + return false, nil + } + return true, nil + }) + if err != nil { + t.Fatalf("failed to initialize the authenticator: %v", err) + } + + if _, _, err = a.AuthenticateToken(ctx, token); err == nil { + t.Fatalf("expected error but got nil") + } + + metricFamilies, err := legacyregistry.DefaultGatherer.Gather() + if err != nil { + t.Fatalf("failed to gather metrics: %v", err) + } + + jwtIssuerHash := getHash(ts.URL) + var timestamp float64 + for _, family := range metricFamilies { + if family.GetName() != "apiserver_authentication_jwt_authenticator_jwks_fetch_last_timestamp_seconds" { + continue + } + + labelFilter := map[string]string{ + "apiserver_id_hash": "sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37", + "jwt_issuer_hash": jwtIssuerHash, + "result": "failure", + } + if !testutil.LabelsMatch(family.Metric[0], labelFilter) { + t.Fatalf("unexpected metric: %v", family.Metric[0]) + } + + timestamp = *family.Metric[0].Gauge.Value + break + } + if timestamp == 0 { + t.Fatalf("failed to get the timestamp") + } + }) + } +} + +func TestJWKSMetrics(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfigurationJWKSMetrics, true) + ResetMetrics() + + synchronizeTokenIDVerifierForTest = true + signingKey := loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256) + pubKeys := []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + } + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm), + Key: signingKey, + }, nil) + if err != nil { + t.Fatalf("initialize signer: %v", err) + } + openIDConfig := `{ + "issuer": "{{.URL}}", + "jwks_uri": "{{.URL}}/.testing/keys" + }` + + ts := newClaimServer(t, toKeySet(pubKeys), signer, nil, &openIDConfig) + defer ts.Close() + + v := struct { + URL string + }{ + URL: ts.URL, + } + openIDConfig = replace(openIDConfig, &v) + + opts := Options{ + JWTAuthenticator: apiserver.JWTAuthenticator{ + Issuer: apiserver.Issuer{ + URL: ts.URL, + Audiences: []string{"my-client"}, + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "username", + Prefix: ptr.To(""), + }, + }, + }, + now: func() time.Time { return now }, + APIServerID: "testAPIServerID", + } + + // Make the certificate of the helper server available to the authenticator + caBundle := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: ts.Certificate().Raw, + }) + caContent, err := dynamiccertificates.NewStaticCAContent("oidc-authenticator", caBundle) + if err != nil { + t.Fatalf("initialize ca: %v", err) + } + opts.CAContentProvider = caContent + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize the authenticator. + a, err := New(ctx, opts) + if err != nil { + t.Fatalf("initialize authenticator: %v", err) + } + + claims := fmt.Sprintf(`{ +"iss": "%s", +"aud": "my-client", +"username": "jane", +"exp": %d +}`, ts.URL, valid.Unix()) + + jws, err := signer.Sign([]byte(claims)) + if err != nil { + t.Fatalf("sign claims: %v", err) + } + token, err := jws.CompactSerialize() + if err != nil { + t.Fatalf("serialize token: %v", err) + } + + ia, ok := a.(*instrumentedAuthenticator) + if !ok { + t.Fatalf("expected authenticator to be instrumented") + } + authenticator, ok := ia.delegate.(*jwtAuthenticator) + if !ok { + t.Fatalf("expected delegate to be Authenticator") + } + // wait for the authenticator to be initialized + err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) { + if v, _ := authenticator.idTokenVerifier(); v == nil { + return false, nil + } + return true, nil + }) + if err != nil { + t.Fatalf("failed to initialize the authenticator: %v", err) + } + + jwtIssuerHash := getHash(ts.URL) + // 1. Remote JWKS fetch success the first time we fetch the keys. + if _, _, err = a.AuthenticateToken(ctx, token); err != nil { + t.Fatalf("authenticate token: %v", err) + } + checkJWKSMetrics(t, jwtIssuerHash, "sha256:6c22a3ad4bfe350786cb03d06c6e6d40a6d642d168935b88f1e72c407d969c83") + + // 2. Rotate the keys and update the server. + signingKey = loadRSAPrivKey(t, "testdata/rsa_2.pem", jose.RS256) + pubKeys = []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256), + } + signer, err = jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm), + Key: signingKey, + }, nil) + if err != nil { + t.Fatalf("initialize signer: %v", err) + } + ts.setKeys(toKeySet(pubKeys)) + // Sign and serialize the claims in a JWT with the new key. + // This should trigger a new JWKS fetch as kid not found in the cache. + jws, err = signer.Sign([]byte(claims)) + if err != nil { + t.Fatalf("sign claims: %v", err) + } + token, err = jws.CompactSerialize() + if err != nil { + t.Fatalf("serialize token: %v", err) + } + if _, _, err = a.AuthenticateToken(ctx, token); err != nil { + t.Fatalf("authenticate token: %v", err) + } + checkJWKSMetrics(t, jwtIssuerHash, "sha256:42444728eaa3a55f7a0192d60cfea88ff5a69ecb7bf9ef0d2ffa3f9db9ceff41") + + // 3. Cancel the context and verify metrics are NOT updated after cancellation. + // Capture current metrics before cancellation. + metricsBeforeCancel := captureTimestampMetric(t, jwtIssuerHash, "sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37") + + // Cancel the authenticator's lifecycle context + cancel() + + // Rotate keys again to trigger another JWKS fetch attempt. + // This should fail because the context is cancelled, but more importantly, + // even if the RoundTrip happens, metrics should NOT be recorded. + signingKey = loadRSAPrivKey(t, "testdata/rsa_3.pem", jose.RS256) + pubKeys = []*jose.JSONWebKey{ + loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), + loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256), + loadRSAKey(t, "testdata/rsa_3.pem", jose.RS256), + } + signer, err = jose.NewSigner(jose.SigningKey{ + Algorithm: jose.SignatureAlgorithm(signingKey.Algorithm), + Key: signingKey, + }, nil) + if err != nil { + t.Fatalf("initialize signer: %v", err) + } + ts.setKeys(toKeySet(pubKeys)) + + // Try to authenticate with a new token (this will likely fail due to cancelled context, + // but we're testing that even if any HTTP request happens, metrics won't be updated) + jws, err = signer.Sign([]byte(claims)) + if err != nil { + t.Fatalf("sign claims: %v", err) + } + token, err = jws.CompactSerialize() + if err != nil { + t.Fatalf("serialize token: %v", err) + } + // Authentication will likely fail with cancelled context, which is expected + _, _, err = a.AuthenticateToken(context.Background(), token) + if err == nil || !strings.Contains(err.Error(), "context canceled") { + t.Fatalf("expected context canceled error, got: %v", err) + } + + // Verify metrics did NOT change after context cancellation + metricsAfterCancel := captureTimestampMetric(t, jwtIssuerHash, "sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37") + if metricsAfterCancel != metricsBeforeCancel { + t.Errorf("metrics changed after context cancellation: before=%v, after=%v (should remain unchanged)", metricsBeforeCancel, metricsAfterCancel) + } +} + +func captureTimestampMetric(t *testing.T, jwtIssuerHash, apiServerIDHash string) float64 { + t.Helper() + + metricFamilies, err := legacyregistry.DefaultGatherer.Gather() + if err != nil { + t.Fatalf("failed to gather metrics: %v", err) + } + + for _, family := range metricFamilies { + if family.GetName() != "apiserver_authentication_jwt_authenticator_jwks_fetch_last_timestamp_seconds" { + continue + } + + for _, metric := range family.Metric { + labelFilter := map[string]string{ + "apiserver_id_hash": apiServerIDHash, + "jwt_issuer_hash": jwtIssuerHash, + "result": "success", + } + if testutil.LabelsMatch(metric, labelFilter) { + return *metric.Gauge.Value + } + } + } + + return 0 +} + +func checkJWKSMetrics(t *testing.T, jwtIssuerHash string, expectedJWKSHash string) { + t.Helper() + + expectedMetricValue := fmt.Sprintf(` + # HELP apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info [ALPHA] Information about the last JWKS fetched by the JWT authenticator with hash as label, split by api server identity and jwt issuer. + # TYPE apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info gauge + apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info{apiserver_id_hash="sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37",hash="%s",jwt_issuer_hash="%s"} 1 + `, expectedJWKSHash, jwtIssuerHash) + + if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedMetricValue), "apiserver_authentication_jwt_authenticator_jwks_fetch_last_key_set_info"); err != nil { + t.Fatalf("unexpected metrics:\n%s", err) + } + + metricFamilies, err := legacyregistry.DefaultGatherer.Gather() + if err != nil { + t.Fatalf("failed to gather metrics: %v", err) + } + + var ts float64 + for _, family := range metricFamilies { + if family.GetName() != "apiserver_authentication_jwt_authenticator_jwks_fetch_last_timestamp_seconds" { + continue + } + + labelFilter := map[string]string{ + "apiserver_id_hash": "sha256:14f9d63e669337ac6bfda2e2162915ee6a6067743eddd4e5c374b572f951ff37", + "jwt_issuer_hash": jwtIssuerHash, + "result": "success", + } + if !testutil.LabelsMatch(family.Metric[0], labelFilter) { + t.Fatalf("unexpected metric: %v", family.Metric[0]) + } + + ts = *family.Metric[0].Gauge.Value + break + } + if ts == 0 { + t.Fatalf("failed to get the timestamp") + } +} + type errTransport string func (e errTransport) RoundTrip(_ *http.Request) (*http.Response, error) { diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_1.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_1.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_1.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_1.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_2.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_2.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_2.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_2.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_3.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_3.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_3.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/ecdsa_3.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/gen.sh b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/gen.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/gen.sh rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/gen.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_1.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_1.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_1.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_1.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_2.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_2.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_2.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_2.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_3.pem b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_3.pem similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_3.pem rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/oidc/testdata/rsa_3.pem diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/tokentest/tokentest.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/tokentest/tokentest.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/tokentest/tokentest.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/tokentest/tokentest.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/certs_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/certs_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/certs_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/certs_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/round_trip_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/round_trip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/round_trip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/round_trip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook_v1_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authenticator/token/webhook/webhook_v1beta1_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/certs_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/certs_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/certs_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/certs_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/gencerts.sh b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/gencerts.sh similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/gencerts.sh rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/gencerts.sh diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics/metrics.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics/metrics.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics/metrics.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics/metrics.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics/metrics_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics/metrics_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics_test.go similarity index 91% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics_test.go index 9fa48014d..b8f7e7231 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/metrics_test.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/metrics_test.go @@ -107,9 +107,16 @@ func TestAuthorizerMetrics(t *testing.T) { if service.statusCode == 0 { service.statusCode = 200 } - service.reviewHook = func(*authorizationv1.SubjectAccessReview) { - if scenario.canceledRequest { + + testFinishedCtx, testFinishedCancel := context.WithCancel(context.Background()) + defer testFinishedCancel() + if scenario.canceledRequest { + service.reviewHook = func(*authorizationv1.SubjectAccessReview) { cancel() + // net/http transport still attempts to use a response if it's + // available right when it's handling context cancellation, + // we need to delay the response. + <-testFinishedCtx.Done() } } service.allow = !scenario.authFakeServiceDeny @@ -120,6 +127,9 @@ func TestAuthorizerMetrics(t *testing.T) { return } defer server.Close() + // testFinishedCtx must be cancelled before we close the server, otherwise + // closing the server hangs on an active connection from the listener. + defer testFinishedCancel() fakeAuthzMetrics := &fakeAuthorizerMetrics{} wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, fakeAuthzMetrics, cel.NewDefaultCompiler(), []apiserver.WebhookMatchCondition{}, "") diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/round_trip_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/round_trip_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/round_trip_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/round_trip_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook.go similarity index 96% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook.go index 3df8e580e..65bf08ee5 100644 --- a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook.go +++ b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook.go @@ -204,21 +204,18 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri Verb: attr.GetVerb(), } } - // skipping match when feature is not enabled - if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) { - // Process Match Conditions before calling the webhook - matches, err := w.match(ctx, r) - // If at least one matchCondition evaluates to an error (but none are FALSE): - // If failurePolicy=Deny, then the webhook rejects the request - // If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped - if err != nil { - return w.decisionOnError, "", err - } - // If at least one matchCondition successfully evaluates to FALSE, - // then the webhook is skipped. - if !matches { - return authorizer.DecisionNoOpinion, "", nil - } + // Process Match Conditions before calling the webhook + matches, err := w.match(ctx, r) + // If at least one matchCondition evaluates to an error (but none are FALSE): + // If failurePolicy=Deny, then the webhook rejects the request + // If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped + if err != nil { + return w.decisionOnError, "", err + } + // If at least one matchCondition successfully evaluates to FALSE, + // then the webhook is skipped. + if !matches { + return authorizer.DecisionNoOpinion, "", nil } // If all evaluated successfully and ALL matchConditions evaluate to TRUE, // then the webhook is called. diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_v1_test.go diff --git a/third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go b/third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go similarity index 100% rename from third_party/k8s.io/apiserver-v0.34.1/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go rename to third_party/k8s.io/apiserver-v0.36.1/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go