216 lines
6.6 KiB
Go
216 lines
6.6 KiB
Go
/*
|
|
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.
|
|
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
|
|
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) 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)
|
|
|
|
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: %v", 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)
|
|
}
|