Merge pull request #122919 from alexzielenski/apiserver/policy/mutating-initial
Refactor AdmissionPolicy for code sharing with mutating
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
apiextensionsscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
|
apiextensionsscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
|
||||||
pluginvalidatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
pluginvalidatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
k8sscheme "k8s.io/client-go/kubernetes/scheme"
|
k8sscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
@@ -31,7 +31,7 @@ import (
|
|||||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||||
"k8s.io/apiserver/pkg/cel"
|
"k8s.io/apiserver/pkg/cel"
|
||||||
"k8s.io/apiserver/pkg/cel/environment"
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
|
@@ -26,7 +26,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
admissionregistrationv1beta1apply "k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1"
|
admissionregistrationv1beta1apply "k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1"
|
||||||
informerv1beta1 "k8s.io/client-go/informers/admissionregistration/v1beta1"
|
informerv1beta1 "k8s.io/client-go/informers/admissionregistration/v1beta1"
|
||||||
admissionregistrationv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
|
admissionregistrationv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
|
||||||
|
@@ -27,7 +27,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
|
"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
@@ -20,7 +20,7 @@ package options
|
|||||||
// This should probably be part of some configuration fed into the build for a
|
// This should probably be part of some configuration fed into the build for a
|
||||||
// given binary target.
|
// given binary target.
|
||||||
import (
|
import (
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
// Admission policies
|
// Admission policies
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||||
|
@@ -54,13 +54,22 @@ func NewCompositedCompiler(envSet *environment.EnvSet) (*CompositedCompiler, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
compiler := NewCompiler(compositionContext.EnvSet)
|
return NewCompositedCompilerFromTemplate(compositionContext), nil
|
||||||
filterCompiler := NewFilterCompiler(compositionContext.EnvSet)
|
}
|
||||||
|
|
||||||
|
func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompiler {
|
||||||
|
context = &CompositionEnv{
|
||||||
|
MapType: context.MapType,
|
||||||
|
EnvSet: context.EnvSet,
|
||||||
|
CompiledVariables: map[string]CompilationResult{},
|
||||||
|
}
|
||||||
|
compiler := NewCompiler(context.EnvSet)
|
||||||
|
filterCompiler := NewFilterCompiler(context.EnvSet)
|
||||||
return &CompositedCompiler{
|
return &CompositedCompiler{
|
||||||
Compiler: compiler,
|
Compiler: compiler,
|
||||||
FilterCompiler: filterCompiler,
|
FilterCompiler: filterCompiler,
|
||||||
CompositionEnv: compositionContext,
|
CompositionEnv: context,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CompositedCompiler) CompileAndStoreVariables(variables []NamedExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) {
|
func (c *CompositedCompiler) CompileAndStoreVariables(variables []NamedExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) {
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PolicyAccessor interface {
|
||||||
|
GetName() string
|
||||||
|
GetNamespace() string
|
||||||
|
GetParamKind() *schema.GroupVersionKind
|
||||||
|
}
|
||||||
|
|
||||||
|
type BindingAccessor interface {
|
||||||
|
GetName() string
|
||||||
|
GetNamespace() string
|
||||||
|
|
||||||
|
// GetPolicyName returns the name of the (Validating/Mutating)AdmissionPolicy,
|
||||||
|
// which is cluster-scoped, so namespace is usually left blank.
|
||||||
|
// But we leave the door open to add a namespaced vesion in the future
|
||||||
|
GetPolicyName() types.NamespacedName
|
||||||
|
}
|
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hook represents a dynamic admission hook. The hook may be a webhook or a
|
||||||
|
// policy. For webhook, the Hook may describe how to contact the endpoint, expected
|
||||||
|
// cert, etc. For policies, the hook may describe a compiled policy-binding pair.
|
||||||
|
type Hook interface {
|
||||||
|
// All hooks are expected to contain zero or more match conditions, object
|
||||||
|
// selectors, namespace selectors to help the dispatcher decide when to apply
|
||||||
|
// the hook.
|
||||||
|
//
|
||||||
|
// Methods of matching logic is applied are specific to the hook and left up
|
||||||
|
// to the implementation.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source can list dynamic admission plugins.
|
||||||
|
type Source[H Hook] interface {
|
||||||
|
// Hooks returns the list of currently known admission hooks.
|
||||||
|
Hooks() []H
|
||||||
|
|
||||||
|
// Run the source. This method should be called only once at startup.
|
||||||
|
Run(ctx context.Context) error
|
||||||
|
|
||||||
|
// HasSynced returns true if the source has completed its initial sync.
|
||||||
|
HasSynced() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatcher dispatches evaluates an admission request against the currently
|
||||||
|
// active hooks returned by the source.
|
||||||
|
type Dispatcher[H Hook] interface {
|
||||||
|
// Dispatch a request to the policies. Dispatcher may choose not to
|
||||||
|
// call a hook, either because the rules of the hook does not match, or
|
||||||
|
// the namespaceSelector or the objectSelector of the hook does not
|
||||||
|
// match. A non-nil error means the request is rejected.
|
||||||
|
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []H) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// An evaluator represents a compiled CEL expression that can be evaluated a
|
||||||
|
// given a set of inputs used by the generic PolicyHook for Mutating and
|
||||||
|
// ValidatingAdmissionPolicy.
|
||||||
|
// Mutating and Validating may have different forms of evaluators
|
||||||
|
type Evaluator interface {
|
||||||
|
}
|
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"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]
|
||||||
|
|
||||||
|
type Invocation struct {
|
||||||
|
Resource schema.GroupVersionResource
|
||||||
|
Subresource string
|
||||||
|
Kind schema.GroupVersionKind
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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]{}
|
||||||
|
_ 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 isPolicyResource(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 isPolicyResource(attr admission.Attributes) bool {
|
||||||
|
gvk := attr.GetResource()
|
||||||
|
if gvk.Group == "admissionregistration.k8s.io" {
|
||||||
|
if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
|
||||||
|
return true
|
||||||
|
} else if gvk.Resource == "mutatingadmissionpolicies" || gvk.Resource == "mutatingadmissionpolicybindings" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@@ -0,0 +1,461 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
goerrors "errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||||
|
ctx context.Context
|
||||||
|
policyInformer generic.Informer[P]
|
||||||
|
bindingInformer generic.Informer[B]
|
||||||
|
restMapper meta.RESTMapper
|
||||||
|
newPolicyAccessor func(P) PolicyAccessor
|
||||||
|
newBindingAccessor func(B) BindingAccessor
|
||||||
|
|
||||||
|
informerFactory informers.SharedInformerFactory
|
||||||
|
dynamicClient dynamic.Interface
|
||||||
|
|
||||||
|
compiler func(P) E
|
||||||
|
|
||||||
|
// Currently compiled list of valid/active policy-binding pairs
|
||||||
|
policies atomic.Pointer[[]PolicyHook[P, B, E]]
|
||||||
|
// Whether the cache of policies is dirty and needs to be recompiled
|
||||||
|
policiesDirty atomic.Bool
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
compiledPolicies map[types.NamespacedName]compiledPolicyEntry[E]
|
||||||
|
|
||||||
|
// Temporary until we use the dynamic informer factory
|
||||||
|
paramsCRDControllers map[schema.GroupVersionKind]*paramInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramInfo struct {
|
||||||
|
mapping meta.RESTMapping
|
||||||
|
|
||||||
|
// When the param is changed, or the informer is done being used, the cancel
|
||||||
|
// func should be called to stop/cleanup the original informer
|
||||||
|
cancelFunc func()
|
||||||
|
|
||||||
|
// The lister for this param
|
||||||
|
informer informers.GenericInformer
|
||||||
|
}
|
||||||
|
|
||||||
|
type compiledPolicyEntry[E Evaluator] struct {
|
||||||
|
policyVersion string
|
||||||
|
evaluator E
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyHook[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||||
|
Policy P
|
||||||
|
Bindings []B
|
||||||
|
ParamInformer informers.GenericInformer
|
||||||
|
ParamScope meta.RESTScope
|
||||||
|
|
||||||
|
Evaluator E
|
||||||
|
ConfigurationError error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Source[PolicyHook[runtime.Object, runtime.Object, Evaluator]] = &policySource[runtime.Object, runtime.Object, Evaluator]{}
|
||||||
|
|
||||||
|
func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator](
|
||||||
|
policyInformer cache.SharedIndexInformer,
|
||||||
|
bindingInformer cache.SharedIndexInformer,
|
||||||
|
newPolicyAccessor func(P) PolicyAccessor,
|
||||||
|
newBindingAccessor func(B) BindingAccessor,
|
||||||
|
compiler func(P) E,
|
||||||
|
paramInformerFactory informers.SharedInformerFactory,
|
||||||
|
dynamicClient dynamic.Interface,
|
||||||
|
restMapper meta.RESTMapper,
|
||||||
|
) Source[PolicyHook[P, B, E]] {
|
||||||
|
res := &policySource[P, B, E]{
|
||||||
|
compiler: compiler,
|
||||||
|
policyInformer: generic.NewInformer[P](policyInformer),
|
||||||
|
bindingInformer: generic.NewInformer[B](bindingInformer),
|
||||||
|
compiledPolicies: map[types.NamespacedName]compiledPolicyEntry[E]{},
|
||||||
|
newPolicyAccessor: newPolicyAccessor,
|
||||||
|
newBindingAccessor: newBindingAccessor,
|
||||||
|
paramsCRDControllers: map[schema.GroupVersionKind]*paramInfo{},
|
||||||
|
informerFactory: paramInformerFactory,
|
||||||
|
dynamicClient: dynamicClient,
|
||||||
|
restMapper: restMapper,
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policySource[P, B, E]) Run(ctx context.Context) error {
|
||||||
|
if s.ctx != nil {
|
||||||
|
return fmt.Errorf("policy source already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for initial cache sync of policies and informers before reconciling
|
||||||
|
// any
|
||||||
|
if !cache.WaitForNamedCacheSync(fmt.Sprintf("%T", s), ctx.Done(), s.UpstreamHasSynced) {
|
||||||
|
err := ctx.Err()
|
||||||
|
if err == nil {
|
||||||
|
err = fmt.Errorf("initial cache sync for %T failed", s)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ctx = ctx
|
||||||
|
|
||||||
|
// Perform initial policy compilation after initial list has finished
|
||||||
|
s.notify()
|
||||||
|
s.refreshPolicies()
|
||||||
|
|
||||||
|
notifyFuncs := cache.ResourceEventHandlerFuncs{
|
||||||
|
AddFunc: func(_ interface{}) {
|
||||||
|
s.notify()
|
||||||
|
},
|
||||||
|
UpdateFunc: func(_, _ interface{}) {
|
||||||
|
s.notify()
|
||||||
|
},
|
||||||
|
DeleteFunc: func(_ interface{}) {
|
||||||
|
s.notify()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handle, err := s.policyInformer.AddEventHandler(notifyFuncs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := s.policyInformer.RemoveEventHandler(handle); err != nil {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("failed to remove policy event handler: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bindingHandle, err := s.bindingInformer.AddEventHandler(notifyFuncs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := s.bindingInformer.RemoveEventHandler(bindingHandle); err != nil {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("failed to remove binding event handler: %v", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start a worker that checks every second to see if policy data is dirty
|
||||||
|
// and needs to be recompiled
|
||||||
|
go func() {
|
||||||
|
// Loop every 1 second until context is cancelled, refreshing policies
|
||||||
|
wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done())
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policySource[P, B, E]) UpstreamHasSynced() bool {
|
||||||
|
return s.policyInformer.HasSynced() && s.bindingInformer.HasSynced()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasSynced implements Source.
|
||||||
|
func (s *policySource[P, B, E]) HasSynced() bool {
|
||||||
|
// As an invariant we never store `nil` into the atomic list of processed
|
||||||
|
// policy hooks. If it is nil, then we haven't compiled all the policies
|
||||||
|
// and stored them yet.
|
||||||
|
return s.Hooks() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks implements Source.
|
||||||
|
func (s *policySource[P, B, E]) Hooks() []PolicyHook[P, B, E] {
|
||||||
|
res := s.policies.Load()
|
||||||
|
|
||||||
|
// Error case should not happen since evaluation function never
|
||||||
|
// returns error
|
||||||
|
if res == nil {
|
||||||
|
// Not yet synced
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return *res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policySource[P, B, E]) refreshPolicies() {
|
||||||
|
if !s.UpstreamHasSynced() {
|
||||||
|
return
|
||||||
|
} else if !s.policiesDirty.Swap(false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is ok the cache gets marked dirty again between us clearing the
|
||||||
|
// flag and us calculating the policies. The dirty flag would be marked again,
|
||||||
|
// and we'd have a no-op after comparing resource versions on the next sync.
|
||||||
|
klog.Infof("refreshing policies")
|
||||||
|
policies, err := s.calculatePolicyData()
|
||||||
|
|
||||||
|
// Intentionally store policy list regardless of error. There may be
|
||||||
|
// an error returned if there was a configuration error in one of the policies,
|
||||||
|
// but we would still want those policies evaluated
|
||||||
|
// (for instance to return error on failaction). Or if there was an error
|
||||||
|
// listing all policies at all, we would want to wipe the list.
|
||||||
|
s.policies.Store(&policies)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// An error was generated while syncing policies. Mark it as dirty again
|
||||||
|
// so we can retry later
|
||||||
|
utilruntime.HandleError(fmt.Errorf("encountered error syncing policies: %v. Rescheduling policy sync", err))
|
||||||
|
s.notify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *policySource[P, B, E]) notify() {
|
||||||
|
s.policiesDirty.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculatePolicyData calculates the list of policies and bindings for each
|
||||||
|
// policy. If there is an error in generation, it will return the error and
|
||||||
|
// the partial list of policies that were able to be generated. Policies that
|
||||||
|
// have an error will have a non-nil ConfigurationError field, but still be
|
||||||
|
// included in the result.
|
||||||
|
//
|
||||||
|
// This function caches the result of the intermediate compilations
|
||||||
|
func (s *policySource[P, B, E]) calculatePolicyData() ([]PolicyHook[P, B, E], error) {
|
||||||
|
if !s.UpstreamHasSynced() {
|
||||||
|
return nil, fmt.Errorf("cannot calculate policy data until upstream has synced")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fat-fingered lock that can be made more fine-tuned if required
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
// Create a local copy of all policies and bindings
|
||||||
|
policiesToBindings := map[types.NamespacedName][]B{}
|
||||||
|
bindingList, err := s.bindingInformer.List(labels.Everything())
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen unless types are misconfigured
|
||||||
|
// (can't use meta.accessor on them)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather a list of all active policy bindings
|
||||||
|
for _, bindingSpec := range bindingList {
|
||||||
|
bindingAccessor := s.newBindingAccessor(bindingSpec)
|
||||||
|
policyKey := bindingAccessor.GetPolicyName()
|
||||||
|
|
||||||
|
// Add this binding to the list of bindings for this policy
|
||||||
|
policiesToBindings[policyKey] = append(policiesToBindings[policyKey], bindingSpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]PolicyHook[P, B, E], 0, len(bindingList))
|
||||||
|
usedParams := map[schema.GroupVersionKind]struct{}{}
|
||||||
|
var errs []error
|
||||||
|
for policyKey, bindingSpecs := range policiesToBindings {
|
||||||
|
var inf generic.NamespacedLister[P] = s.policyInformer
|
||||||
|
if len(policyKey.Namespace) > 0 {
|
||||||
|
inf = s.policyInformer.Namespaced(policyKey.Namespace)
|
||||||
|
}
|
||||||
|
policySpec, err := inf.Get(policyKey.Name)
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
// Policy for bindings doesn't exist. This can happen if the policy
|
||||||
|
// was deleted before the binding, or the binding was created first.
|
||||||
|
//
|
||||||
|
// Just skip bindings that refer to non-existent policies
|
||||||
|
// If the policy is recreated, the cache will be marked dirty and
|
||||||
|
// this function will run again.
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
// This should never happen since fetching from a cache should never
|
||||||
|
// fail and this function checks that the cache was synced before
|
||||||
|
// even getting to this point.
|
||||||
|
errs = append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
policyAccessor := s.newPolicyAccessor(policySpec)
|
||||||
|
paramInformer, paramScope, configurationError := s.ensureParamsForPolicyLocked(policyAccessor.GetParamKind())
|
||||||
|
result = append(result, PolicyHook[P, B, E]{
|
||||||
|
Policy: policySpec,
|
||||||
|
Bindings: bindingSpecs,
|
||||||
|
Evaluator: s.compilePolicyLocked(policySpec),
|
||||||
|
ParamInformer: paramInformer,
|
||||||
|
ParamScope: paramScope,
|
||||||
|
ConfigurationError: configurationError,
|
||||||
|
})
|
||||||
|
|
||||||
|
// TEMPORARY UNTIL WE HAVE SHARED PARAM INFORMERS
|
||||||
|
if paramKind := policyAccessor.GetParamKind(); paramKind != nil {
|
||||||
|
usedParams[*paramKind] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should queue a re-sync for policy sync error. If our shared param
|
||||||
|
// informer can notify us when CRD discovery changes we can remove this
|
||||||
|
// and just rely on the informer to notify us when the CRDs change
|
||||||
|
if configurationError != nil {
|
||||||
|
errs = append(errs, configurationError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up orphaned policies by replacing the old cache of compiled policies
|
||||||
|
// (the map of used policies is updated by `compilePolicy`)
|
||||||
|
for policyKey := range s.compiledPolicies {
|
||||||
|
if _, wasSeen := policiesToBindings[policyKey]; !wasSeen {
|
||||||
|
delete(s.compiledPolicies, policyKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up orphaned param informers
|
||||||
|
for paramKind, info := range s.paramsCRDControllers {
|
||||||
|
if _, wasSeen := usedParams[paramKind]; !wasSeen {
|
||||||
|
info.cancelFunc()
|
||||||
|
delete(s.paramsCRDControllers, paramKind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
if len(errs) > 0 {
|
||||||
|
err = goerrors.Join(errs...)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureParamsForPolicyLocked ensures that the informer for the paramKind is
|
||||||
|
// started and returns the informer and the scope of the paramKind.
|
||||||
|
//
|
||||||
|
// Must be called under write lock
|
||||||
|
func (s *policySource[P, B, E]) ensureParamsForPolicyLocked(paramSource *schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope, error) {
|
||||||
|
if paramSource == nil {
|
||||||
|
return nil, nil, nil
|
||||||
|
} else if info, ok := s.paramsCRDControllers[*paramSource]; ok {
|
||||||
|
return info.informer, info.mapping.Scope, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping, err := s.restMapper.RESTMapping(schema.GroupKind{
|
||||||
|
Group: paramSource.Group,
|
||||||
|
Kind: paramSource.Kind,
|
||||||
|
}, paramSource.Version)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Failed to resolve. Return error so we retry again (rate limited)
|
||||||
|
// Save a record of this definition with an evaluator that unconditionally
|
||||||
|
return nil, nil, fmt.Errorf("failed to find resource referenced by paramKind: '%v'", *paramSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are not watching this param. Start an informer for it.
|
||||||
|
instanceContext, instanceCancel := context.WithCancel(s.ctx)
|
||||||
|
|
||||||
|
var informer informers.GenericInformer
|
||||||
|
|
||||||
|
// Try to see if our provided informer factory has an informer for this type.
|
||||||
|
// We assume the informer is already started, and starts all types associated
|
||||||
|
// with it.
|
||||||
|
if genericInformer, err := s.informerFactory.ForResource(mapping.Resource); err == nil {
|
||||||
|
informer = genericInformer
|
||||||
|
|
||||||
|
// Start the informer
|
||||||
|
s.informerFactory.Start(instanceContext.Done())
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Dynamic JSON informer fallback.
|
||||||
|
// Cannot use shared dynamic informer since it would be impossible
|
||||||
|
// to clean CRD informers properly with multiple dependents
|
||||||
|
// (cannot start ahead of time, and cannot track dependencies via stopCh)
|
||||||
|
informer = dynamicinformer.NewFilteredDynamicInformer(
|
||||||
|
s.dynamicClient,
|
||||||
|
mapping.Resource,
|
||||||
|
corev1.NamespaceAll,
|
||||||
|
// Use same interval as is used for k8s typed sharedInformerFactory
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430
|
||||||
|
10*time.Minute,
|
||||||
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
go informer.Informer().Run(instanceContext.Done())
|
||||||
|
}
|
||||||
|
|
||||||
|
klog.Infof("informer started for %v", *paramSource)
|
||||||
|
ret := ¶mInfo{
|
||||||
|
mapping: *mapping,
|
||||||
|
cancelFunc: instanceCancel,
|
||||||
|
informer: informer,
|
||||||
|
}
|
||||||
|
s.paramsCRDControllers[*paramSource] = ret
|
||||||
|
return ret.informer, mapping.Scope, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
func (s *policySource[P, B, E]) getParamInformer(param schema.GroupVersionKind) (informers.GenericInformer, meta.RESTScope) {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
if info, ok := s.paramsCRDControllers[param]; ok {
|
||||||
|
return info.informer, info.mapping.Scope
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compilePolicyLocked compiles the policy and returns the evaluator for it.
|
||||||
|
// If the policy has not changed since the last compilation, it will return
|
||||||
|
// the cached evaluator.
|
||||||
|
//
|
||||||
|
// Must be called under write lock
|
||||||
|
func (s *policySource[P, B, E]) compilePolicyLocked(policySpec P) E {
|
||||||
|
policyMeta, err := meta.Accessor(policySpec)
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen if P, and B have ObjectMeta, but
|
||||||
|
// unfortunately there is no way to express "able to call
|
||||||
|
// meta.Accessor" as a type constraint
|
||||||
|
utilruntime.HandleError(err)
|
||||||
|
var emptyEvaluator E
|
||||||
|
return emptyEvaluator
|
||||||
|
}
|
||||||
|
key := types.NamespacedName{
|
||||||
|
Namespace: policyMeta.GetNamespace(),
|
||||||
|
Name: policyMeta.GetName(),
|
||||||
|
}
|
||||||
|
|
||||||
|
compiledPolicy, wasCompiled := s.compiledPolicies[key]
|
||||||
|
|
||||||
|
// If the policy or binding has changed since it was last compiled,
|
||||||
|
// and if there is no configuration error (like a missing param CRD)
|
||||||
|
// then we recompile
|
||||||
|
if !wasCompiled ||
|
||||||
|
compiledPolicy.policyVersion != policyMeta.GetResourceVersion() {
|
||||||
|
|
||||||
|
compiledPolicy = compiledPolicyEntry[E]{
|
||||||
|
policyVersion: policyMeta.GetResourceVersion(),
|
||||||
|
evaluator: s.compiler(policySpec),
|
||||||
|
}
|
||||||
|
s.compiledPolicies[key] = compiledPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
return compiledPolicy.evaluator
|
||||||
|
}
|
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicySourceHasSyncedEmpty(t *testing.T) {
|
||||||
|
testContext, testCancel, err := generic.NewPolicyTestContext(
|
||||||
|
func(fp *FakePolicy) generic.PolicyAccessor { return fp },
|
||||||
|
func(fb *FakeBinding) generic.BindingAccessor { return fb },
|
||||||
|
func(fp *FakePolicy) generic.Evaluator { return nil },
|
||||||
|
makeTestDispatcher,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer testCancel()
|
||||||
|
require.NoError(t, testContext.Start())
|
||||||
|
|
||||||
|
// Should be able to wait for cache sync
|
||||||
|
require.True(t, cache.WaitForCacheSync(testContext.Done(), testContext.Source.HasSynced), "cache should sync after informer running")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicySourceHasSyncedInitialList(t *testing.T) {
|
||||||
|
// Create a list of fake policies
|
||||||
|
initialObjects := []runtime.Object{
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&FakeBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "binding1",
|
||||||
|
},
|
||||||
|
PolicyName: "policy1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext, testCancel, err := generic.NewPolicyTestContext(
|
||||||
|
func(fp *FakePolicy) generic.PolicyAccessor { return fp },
|
||||||
|
func(fb *FakeBinding) generic.BindingAccessor { return fb },
|
||||||
|
func(fp *FakePolicy) generic.Evaluator { return nil },
|
||||||
|
makeTestDispatcher,
|
||||||
|
initialObjects,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer testCancel()
|
||||||
|
require.NoError(t, testContext.Start())
|
||||||
|
// Should be able to wait for cache sync
|
||||||
|
require.Len(t, testContext.Source.Hooks(), 1, "should have one policy")
|
||||||
|
require.NoError(t, testContext.UpdateAndWait(
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&FakeBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "binding2",
|
||||||
|
},
|
||||||
|
PolicyName: "policy2",
|
||||||
|
},
|
||||||
|
))
|
||||||
|
require.Len(t, testContext.Source.Hooks(), 2, "should have two policies")
|
||||||
|
require.NoError(t, testContext.UpdateAndWait(
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&FakeBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "binding3",
|
||||||
|
},
|
||||||
|
PolicyName: "policy3",
|
||||||
|
},
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy2",
|
||||||
|
},
|
||||||
|
ParamKind: &schema.GroupVersionKind{
|
||||||
|
Group: "policy.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "FakeParam",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
))
|
||||||
|
require.Len(t, testContext.Source.Hooks(), 3, "should have 3 policies")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicySourceBindsToPolicies(t *testing.T) {
|
||||||
|
// Create a list of fake policies
|
||||||
|
initialObjects := []runtime.Object{
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&FakeBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "binding1",
|
||||||
|
},
|
||||||
|
PolicyName: "policy1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext, testCancel, err := generic.NewPolicyTestContext(
|
||||||
|
func(fp *FakePolicy) generic.PolicyAccessor { return fp },
|
||||||
|
func(fb *FakeBinding) generic.BindingAccessor { return fb },
|
||||||
|
func(fp *FakePolicy) generic.Evaluator { return nil },
|
||||||
|
makeTestDispatcher,
|
||||||
|
initialObjects,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer testCancel()
|
||||||
|
require.NoError(t, testContext.Start())
|
||||||
|
|
||||||
|
require.Len(t, testContext.Source.Hooks(), 1, "should have one policy")
|
||||||
|
require.Len(t, testContext.Source.Hooks()[0].Bindings, 1, "should have one binding")
|
||||||
|
require.Equal(t, "binding1", testContext.Source.Hooks()[0].Bindings[0].GetName(), "should have one binding")
|
||||||
|
|
||||||
|
// Change the binding to another policy (policies without bindings should
|
||||||
|
// be ignored, so it should remove the first
|
||||||
|
require.NoError(t, testContext.UpdateAndWait(
|
||||||
|
&FakePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "policy2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&FakeBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "binding1",
|
||||||
|
},
|
||||||
|
PolicyName: "policy2",
|
||||||
|
}))
|
||||||
|
require.Len(t, testContext.Source.Hooks(), 1, "should have one policy")
|
||||||
|
require.Equal(t, "policy2", testContext.Source.Hooks()[0].Policy.GetName(), "policy name should be policy2")
|
||||||
|
require.Len(t, testContext.Source.Hooks()[0].Bindings, 1, "should have one binding")
|
||||||
|
require.Equal(t, "binding1", testContext.Source.Hooks()[0].Bindings[0].GetName(), "binding name should be binding1")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakePolicy struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
ParamKind *schema.GroupVersionKind
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ generic.PolicyAccessor = &FakePolicy{}
|
||||||
|
|
||||||
|
type FakeBinding struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ObjectMeta
|
||||||
|
|
||||||
|
PolicyName string
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ generic.BindingAccessor = &FakeBinding{}
|
||||||
|
|
||||||
|
func (fp *FakePolicy) GetName() string {
|
||||||
|
return fp.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FakePolicy) GetNamespace() string {
|
||||||
|
return fp.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FakePolicy) GetParamKind() *schema.GroupVersionKind {
|
||||||
|
return fp.ParamKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FakeBinding) GetName() string {
|
||||||
|
return fb.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FakeBinding) GetNamespace() string {
|
||||||
|
return fb.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FakeBinding) GetPolicyName() types.NamespacedName {
|
||||||
|
return types.NamespacedName{
|
||||||
|
Name: fb.PolicyName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FakePolicy) DeepCopyObject() runtime.Object {
|
||||||
|
// totally fudged deepcopy
|
||||||
|
newFP := &FakePolicy{}
|
||||||
|
*newFP = *fp
|
||||||
|
return newFP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fb *FakeBinding) DeepCopyObject() runtime.Object {
|
||||||
|
// totally fudged deepcopy
|
||||||
|
newFB := &FakeBinding{}
|
||||||
|
*newFB = *fb
|
||||||
|
return newFB
|
||||||
|
}
|
@@ -0,0 +1,608 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
clienttesting "k8s.io/client-go/testing"
|
||||||
|
"k8s.io/client-go/tools/cache"
|
||||||
|
"k8s.io/component-base/featuregate"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PolicyTestContext is everything you need to unit test a policy plugin
|
||||||
|
type PolicyTestContext[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||||
|
context.Context
|
||||||
|
Plugin *Plugin[PolicyHook[P, B, E]]
|
||||||
|
Source Source[PolicyHook[P, B, E]]
|
||||||
|
Start func() error
|
||||||
|
|
||||||
|
scheme *runtime.Scheme
|
||||||
|
restMapper *meta.DefaultRESTMapper
|
||||||
|
policyGVR schema.GroupVersionResource
|
||||||
|
bindingGVR schema.GroupVersionResource
|
||||||
|
|
||||||
|
policyGVK schema.GroupVersionKind
|
||||||
|
bindingGVK schema.GroupVersionKind
|
||||||
|
|
||||||
|
nativeTracker clienttesting.ObjectTracker
|
||||||
|
policyAndBindingTracker clienttesting.ObjectTracker
|
||||||
|
unstructuredTracker clienttesting.ObjectTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPolicyTestContext[P, B runtime.Object, E Evaluator](
|
||||||
|
newPolicyAccessor func(P) PolicyAccessor,
|
||||||
|
newBindingAccessor func(B) BindingAccessor,
|
||||||
|
compileFunc func(P) E,
|
||||||
|
dispatcher dispatcherFactory[PolicyHook[P, B, E]],
|
||||||
|
initialObjects []runtime.Object,
|
||||||
|
paramMappings []meta.RESTMapping,
|
||||||
|
) (*PolicyTestContext[P, B, E], func(), error) {
|
||||||
|
var Pexample P
|
||||||
|
var Bexample B
|
||||||
|
|
||||||
|
// Create a fake resource and kind for the provided policy and binding types
|
||||||
|
fakePolicyGVR := schema.GroupVersionResource{
|
||||||
|
Group: "policy.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "fakepolicies",
|
||||||
|
}
|
||||||
|
fakeBindingGVR := schema.GroupVersionResource{
|
||||||
|
Group: "policy.example.com",
|
||||||
|
Version: "v1",
|
||||||
|
Resource: "fakebindings",
|
||||||
|
}
|
||||||
|
fakePolicyGVK := fakePolicyGVR.GroupVersion().WithKind("FakePolicy")
|
||||||
|
fakeBindingGVK := fakeBindingGVR.GroupVersion().WithKind("FakeBinding")
|
||||||
|
|
||||||
|
policySourceTestScheme, err := func() (*runtime.Scheme, error) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
|
||||||
|
if err := fake.AddToScheme(scheme); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme.AddKnownTypeWithName(fakePolicyGVK, Pexample)
|
||||||
|
scheme.AddKnownTypeWithName(fakeBindingGVK, Bexample)
|
||||||
|
scheme.AddKnownTypeWithName(fakePolicyGVK.GroupVersion().WithKind(fakePolicyGVK.Kind+"List"), &FakeList[P]{})
|
||||||
|
scheme.AddKnownTypeWithName(fakeBindingGVK.GroupVersion().WithKind(fakeBindingGVK.Kind+"List"), &FakeList[B]{})
|
||||||
|
|
||||||
|
for _, mapping := range paramMappings {
|
||||||
|
// Skip if it is in the scheme already
|
||||||
|
if scheme.Recognizes(mapping.GroupVersionKind) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scheme.AddKnownTypeWithName(mapping.GroupVersionKind, &unstructured.Unstructured{})
|
||||||
|
scheme.AddKnownTypeWithName(mapping.GroupVersionKind.GroupVersion().WithKind(mapping.GroupVersionKind.Kind+"List"), &unstructured.UnstructuredList{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheme, nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeRestMapper := func() *meta.DefaultRESTMapper {
|
||||||
|
res := meta.NewDefaultRESTMapper([]schema.GroupVersion{
|
||||||
|
{
|
||||||
|
Group: "",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
res.Add(fakePolicyGVK, meta.RESTScopeRoot)
|
||||||
|
res.Add(fakeBindingGVK, meta.RESTScopeRoot)
|
||||||
|
res.Add(corev1.SchemeGroupVersion.WithKind("ConfigMap"), meta.RESTScopeNamespace)
|
||||||
|
|
||||||
|
for _, mapping := range paramMappings {
|
||||||
|
res.AddSpecific(mapping.GroupVersionKind, mapping.Resource, mapping.Resource, mapping.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}()
|
||||||
|
|
||||||
|
nativeClient := fake.NewSimpleClientset()
|
||||||
|
dynamicClient := dynamicfake.NewSimpleDynamicClient(policySourceTestScheme)
|
||||||
|
fakeInformerFactory := informers.NewSharedInformerFactory(nativeClient, 30*time.Second)
|
||||||
|
|
||||||
|
// Make an object tracker specifically for our policies and bindings
|
||||||
|
policiesAndBindingsTracker := clienttesting.NewObjectTracker(
|
||||||
|
policySourceTestScheme,
|
||||||
|
serializer.NewCodecFactory(policySourceTestScheme).UniversalDecoder())
|
||||||
|
|
||||||
|
// Make an informer for our policies and bindings
|
||||||
|
|
||||||
|
policyInformer := cache.NewSharedIndexInformer(
|
||||||
|
&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, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Pexample,
|
||||||
|
30*time.Second,
|
||||||
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||||
|
)
|
||||||
|
bindingInformer := cache.NewSharedIndexInformer(
|
||||||
|
&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, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Bexample,
|
||||||
|
30*time.Second,
|
||||||
|
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
||||||
|
)
|
||||||
|
|
||||||
|
var source Source[PolicyHook[P, B, E]]
|
||||||
|
plugin := NewPlugin[PolicyHook[P, B, E]](
|
||||||
|
admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
|
||||||
|
func(sif informers.SharedInformerFactory, i1 kubernetes.Interface, i2 dynamic.Interface, r meta.RESTMapper) Source[PolicyHook[P, B, E]] {
|
||||||
|
source = NewPolicySource[P, B, E](
|
||||||
|
policyInformer,
|
||||||
|
bindingInformer,
|
||||||
|
newPolicyAccessor,
|
||||||
|
newBindingAccessor,
|
||||||
|
compileFunc,
|
||||||
|
sif,
|
||||||
|
i2,
|
||||||
|
r,
|
||||||
|
)
|
||||||
|
return source
|
||||||
|
}, dispatcher)
|
||||||
|
plugin.SetEnabled(true)
|
||||||
|
|
||||||
|
featureGate := featuregate.NewFeatureGate()
|
||||||
|
err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
|
||||||
|
//!TODO: move this to validating specific tests
|
||||||
|
features.ValidatingAdmissionPolicy: {
|
||||||
|
Default: true, PreRelease: featuregate.Beta}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
testContext, testCancel := context.WithCancel(context.Background())
|
||||||
|
genericInitializer := initializer.New(
|
||||||
|
nativeClient,
|
||||||
|
dynamicClient,
|
||||||
|
fakeInformerFactory,
|
||||||
|
fakeAuthorizer{},
|
||||||
|
featureGate,
|
||||||
|
testContext.Done(),
|
||||||
|
)
|
||||||
|
genericInitializer.Initialize(plugin)
|
||||||
|
plugin.SetRESTMapper(fakeRestMapper)
|
||||||
|
|
||||||
|
if err := plugin.ValidateInitialization(); err != nil {
|
||||||
|
testCancel()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &PolicyTestContext[P, B, E]{
|
||||||
|
Context: testContext,
|
||||||
|
Plugin: plugin,
|
||||||
|
Source: source,
|
||||||
|
|
||||||
|
restMapper: fakeRestMapper,
|
||||||
|
scheme: policySourceTestScheme,
|
||||||
|
policyGVK: fakePolicyGVK,
|
||||||
|
bindingGVK: fakeBindingGVK,
|
||||||
|
policyGVR: fakePolicyGVR,
|
||||||
|
bindingGVR: fakeBindingGVR,
|
||||||
|
nativeTracker: nativeClient.Tracker(),
|
||||||
|
policyAndBindingTracker: policiesAndBindingsTracker,
|
||||||
|
unstructuredTracker: dynamicClient.Tracker(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range initialObjects {
|
||||||
|
err := res.updateOne(obj)
|
||||||
|
if err != nil {
|
||||||
|
testCancel()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Start = func() error {
|
||||||
|
fakeInformerFactory.Start(res.Done())
|
||||||
|
go policyInformer.Run(res.Done())
|
||||||
|
go bindingInformer.Run(res.Done())
|
||||||
|
|
||||||
|
if !cache.WaitForCacheSync(res.Done(), res.Source.HasSynced) {
|
||||||
|
return fmt.Errorf("timed out waiting for initial cache sync")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return res, testCancel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAndWait updates the given object in the test, or creates it if it doesn't exist
|
||||||
|
// Depending upon object type, waits afterward until the object is synced
|
||||||
|
// by the policy source
|
||||||
|
//
|
||||||
|
// Be aware the UpdateAndWait will modify the ResourceVersion of the
|
||||||
|
// provided objects.
|
||||||
|
func (p *PolicyTestContext[P, B, E]) UpdateAndWait(objects ...runtime.Object) error {
|
||||||
|
return p.update(true, objects...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the given object in the test, or creates it if it doesn't exist
|
||||||
|
//
|
||||||
|
// Be aware the Update will modify the ResourceVersion of the
|
||||||
|
// provided objects.
|
||||||
|
func (p *PolicyTestContext[P, B, E]) Update(objects ...runtime.Object) error {
|
||||||
|
return p.update(false, objects...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects the given object in the test, or creates it if it doesn't exist
|
||||||
|
// Depending upon object type, waits afterward until the object is synced
|
||||||
|
// by the policy source
|
||||||
|
func (p *PolicyTestContext[P, B, E]) update(wait bool, objects ...runtime.Object) error {
|
||||||
|
for _, object := range objects {
|
||||||
|
if err := p.updateOne(object); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
for _, object := range objects {
|
||||||
|
if err := p.WaitForReconcile(timeoutCtx, object); err != nil {
|
||||||
|
return fmt.Errorf("error waiting for reconcile of %v: %v", object, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending upon object type, waits afterward until the object is synced
|
||||||
|
// by the policy source. Note that policies that are not bound are skipped,
|
||||||
|
// so you should not try to wait for an unbound policy. Create both the binding
|
||||||
|
// and policy, then wait.
|
||||||
|
func (p *PolicyTestContext[P, B, E]) WaitForReconcile(timeoutCtx context.Context, object runtime.Object) error {
|
||||||
|
if !p.Source.HasSynced() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
objectMeta, err := meta.Accessor(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
objectGVK := object.GetObjectKind().GroupVersionKind()
|
||||||
|
switch objectGVK {
|
||||||
|
case p.policyGVK:
|
||||||
|
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
policies := p.Source.Hooks()
|
||||||
|
for _, policy := range policies {
|
||||||
|
policyMeta, err := meta.Accessor(policy.Policy)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
} else if policyMeta.GetName() == objectMeta.GetName() && policyMeta.GetResourceVersion() == objectMeta.GetResourceVersion() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
case p.bindingGVK:
|
||||||
|
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
policies := p.Source.Hooks()
|
||||||
|
for _, policy := range policies {
|
||||||
|
for _, binding := range policy.Bindings {
|
||||||
|
bindingMeta, err := meta.Accessor(binding)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
} else if bindingMeta.GetName() == objectMeta.GetName() && bindingMeta.GetResourceVersion() == objectMeta.GetResourceVersion() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Do nothing, params are visible immediately
|
||||||
|
// Loop until one of the params is visible via get of the param informer
|
||||||
|
return wait.PollUntilContextCancel(timeoutCtx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK)
|
||||||
|
if informer == nil {
|
||||||
|
// Informer does not exist yet, keep waiting for sync
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cache.WaitForCacheSync(timeoutCtx.Done(), informer.Informer().HasSynced) {
|
||||||
|
return false, fmt.Errorf("timed out waiting for cache sync of param informer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lister cache.GenericNamespaceLister = informer.Lister()
|
||||||
|
if scope == meta.RESTScopeNamespace {
|
||||||
|
lister = informer.Lister().ByNamespace(objectMeta.GetNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
fetched, err := lister.Get(objectMeta.GetName())
|
||||||
|
if err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure RV matches
|
||||||
|
fetchedMeta, err := meta.Accessor(fetched)
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
} else if fetchedMeta.GetResourceVersion() != objectMeta.GetResourceVersion() {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PolicyTestContext[P, B, E]) waitForDelete(ctx context.Context, objectGVK schema.GroupVersionKind, name types.NamespacedName) error {
|
||||||
|
srce := p.Source.(*policySource[P, B, E])
|
||||||
|
|
||||||
|
return wait.PollUntilContextCancel(ctx, 100*time.Millisecond, true, func(ctx context.Context) (done bool, err error) {
|
||||||
|
switch objectGVK {
|
||||||
|
case p.policyGVK:
|
||||||
|
for _, hook := range p.Source.Hooks() {
|
||||||
|
accessor := srce.newPolicyAccessor(hook.Policy)
|
||||||
|
if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
case p.bindingGVK:
|
||||||
|
for _, hook := range p.Source.Hooks() {
|
||||||
|
for _, binding := range hook.Bindings {
|
||||||
|
accessor := srce.newBindingAccessor(binding)
|
||||||
|
if accessor.GetName() == name.Name && accessor.GetNamespace() == name.Namespace {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
// Do nothing, params are visible immediately
|
||||||
|
// Loop until one of the params is visible via get of the param informer
|
||||||
|
informer, scope := p.Source.(*policySource[P, B, E]).getParamInformer(objectGVK)
|
||||||
|
if informer == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lister cache.GenericNamespaceLister = informer.Lister()
|
||||||
|
if scope == meta.RESTScopeNamespace {
|
||||||
|
lister = informer.Lister().ByNamespace(name.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = lister.Get(name.Name)
|
||||||
|
if err != nil {
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PolicyTestContext[P, B, E]) updateOne(object runtime.Object) error {
|
||||||
|
objectMeta, err := meta.Accessor(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objectMeta.SetResourceVersion(string(uuid.NewUUID()))
|
||||||
|
objectGVK := object.GetObjectKind().GroupVersionKind()
|
||||||
|
|
||||||
|
if objectGVK.Empty() {
|
||||||
|
// If the object doesn't have a GVK, ask the schema for preferred GVK
|
||||||
|
knownKinds, _, err := p.scheme.ObjectKinds(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(knownKinds) == 0 {
|
||||||
|
return fmt.Errorf("no known GVKs for object in schema: %T", object)
|
||||||
|
}
|
||||||
|
toTake := 0
|
||||||
|
|
||||||
|
// Prefer GVK if it is our fake policy or binding
|
||||||
|
for i, knownKind := range knownKinds {
|
||||||
|
if knownKind == p.policyGVK || knownKind == p.bindingGVK {
|
||||||
|
toTake = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectGVK = knownKinds[toTake]
|
||||||
|
object.GetObjectKind().SetGroupVersionKind(objectGVK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure GVK is known to the fake rest mapper. To prevent cryptic error
|
||||||
|
mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch objectGVK {
|
||||||
|
case p.policyGVK:
|
||||||
|
err := p.policyAndBindingTracker.Update(p.policyGVR, object, objectMeta.GetNamespace())
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
err = p.policyAndBindingTracker.Create(p.policyGVR, object, objectMeta.GetNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
case p.bindingGVK:
|
||||||
|
err := p.policyAndBindingTracker.Update(p.bindingGVR, object, objectMeta.GetNamespace())
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
err = p.policyAndBindingTracker.Create(p.bindingGVR, object, objectMeta.GetNamespace())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
if _, ok := object.(*unstructured.Unstructured); ok {
|
||||||
|
if err := p.unstructuredTracker.Create(mapping.Resource, object, objectMeta.GetNamespace()); err != nil {
|
||||||
|
if errors.IsAlreadyExists(err) {
|
||||||
|
return p.unstructuredTracker.Update(mapping.Resource, object, objectMeta.GetNamespace())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
} else if err := p.nativeTracker.Create(mapping.Resource, object, objectMeta.GetNamespace()); err != nil {
|
||||||
|
if errors.IsAlreadyExists(err) {
|
||||||
|
return p.nativeTracker.Update(mapping.Resource, object, objectMeta.GetNamespace())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending upon object type, waits afterward until the object is synced
|
||||||
|
// by the policy source
|
||||||
|
func (p *PolicyTestContext[P, B, E]) DeleteAndWait(object ...runtime.Object) error {
|
||||||
|
for _, object := range object {
|
||||||
|
if err := p.deleteOne(object); err != nil && !errors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(p, 3*time.Second)
|
||||||
|
defer timeoutCancel()
|
||||||
|
|
||||||
|
for _, object := range object {
|
||||||
|
accessor, err := meta.Accessor(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.waitForDelete(
|
||||||
|
timeoutCtx,
|
||||||
|
object.GetObjectKind().GroupVersionKind(),
|
||||||
|
types.NamespacedName{Name: accessor.GetName(), Namespace: accessor.GetNamespace()}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PolicyTestContext[P, B, E]) deleteOne(object runtime.Object) error {
|
||||||
|
objectMeta, err := meta.Accessor(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objectMeta.SetResourceVersion(string(uuid.NewUUID()))
|
||||||
|
objectGVK := object.GetObjectKind().GroupVersionKind()
|
||||||
|
|
||||||
|
if objectGVK.Empty() {
|
||||||
|
// If the object doesn't have a GVK, ask the schema for preferred GVK
|
||||||
|
knownKinds, _, err := p.scheme.ObjectKinds(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(knownKinds) == 0 {
|
||||||
|
return fmt.Errorf("no known GVKs for object in schema: %T", object)
|
||||||
|
}
|
||||||
|
toTake := 0
|
||||||
|
|
||||||
|
// Prefer GVK if it is our fake policy or binding
|
||||||
|
for i, knownKind := range knownKinds {
|
||||||
|
if knownKind == p.policyGVK || knownKind == p.bindingGVK {
|
||||||
|
toTake = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objectGVK = knownKinds[toTake]
|
||||||
|
object.GetObjectKind().SetGroupVersionKind(objectGVK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure GVK is known to the fake rest mapper. To prevent cryptic error
|
||||||
|
mapping, err := p.restMapper.RESTMapping(objectGVK.GroupKind(), objectGVK.Version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch objectGVK {
|
||||||
|
case p.policyGVK:
|
||||||
|
return p.policyAndBindingTracker.Delete(p.policyGVR, objectMeta.GetNamespace(), objectMeta.GetName())
|
||||||
|
case p.bindingGVK:
|
||||||
|
return p.policyAndBindingTracker.Delete(p.bindingGVR, objectMeta.GetNamespace(), objectMeta.GetName())
|
||||||
|
default:
|
||||||
|
if _, ok := object.(*unstructured.Unstructured); ok {
|
||||||
|
return p.unstructuredTracker.Delete(mapping.Resource, objectMeta.GetNamespace(), objectMeta.GetName())
|
||||||
|
}
|
||||||
|
return p.nativeTracker.Delete(mapping.Resource, objectMeta.GetNamespace(), objectMeta.GetName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FakeList[T runtime.Object] struct {
|
||||||
|
metav1.TypeMeta
|
||||||
|
metav1.ListMeta
|
||||||
|
Items []T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fl *FakeList[P]) DeepCopyObject() runtime.Object {
|
||||||
|
copiedItems := make([]P, len(fl.Items))
|
||||||
|
for i, item := range fl.Items {
|
||||||
|
copiedItems[i] = item.DeepCopyObject().(P)
|
||||||
|
}
|
||||||
|
return &FakeList[P]{
|
||||||
|
TypeMeta: fl.TypeMeta,
|
||||||
|
ListMeta: fl.ListMeta,
|
||||||
|
Items: copiedItems,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAuthorizer struct{}
|
||||||
|
|
||||||
|
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
@@ -39,7 +39,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
|
||||||
|
|
||||||
clienttesting "k8s.io/client-go/testing"
|
clienttesting "k8s.io/client-go/testing"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
@@ -28,6 +28,10 @@ type informer[T runtime.Object] struct {
|
|||||||
lister[T]
|
lister[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a generic informer around a type-erased cache.SharedIndexInformer
|
||||||
|
// It is incumbent on the caller to ensure that the generic type argument is
|
||||||
|
// consistent with the type of the objects stored inside the SharedIndexInformer
|
||||||
|
// as they will be casted.
|
||||||
func NewInformer[T runtime.Object](informe cache.SharedIndexInformer) Informer[T] {
|
func NewInformer[T runtime.Object](informe cache.SharedIndexInformer) Informer[T] {
|
||||||
return informer[T]{
|
return informer[T]{
|
||||||
SharedIndexInformer: informe,
|
SharedIndexInformer: informe,
|
@@ -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 validating
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewValidatingAdmissionPolicyAccessor(obj *v1beta1.ValidatingAdmissionPolicy) generic.PolicyAccessor {
|
||||||
|
return &validatingAdmissionPolicyAccessor{
|
||||||
|
ValidatingAdmissionPolicy: obj,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewValidatingAdmissionPolicyBindingAccessor(obj *v1beta1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor {
|
||||||
|
return &validatingAdmissionPolicyBindingAccessor{
|
||||||
|
ValidatingAdmissionPolicyBinding: obj,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validatingAdmissionPolicyAccessor struct {
|
||||||
|
*v1beta1.ValidatingAdmissionPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyAccessor) GetNamespace() string {
|
||||||
|
return v.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyAccessor) GetName() string {
|
||||||
|
return v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyAccessor) GetParamKind() *schema.GroupVersionKind {
|
||||||
|
paramKind := v.Spec.ParamKind
|
||||||
|
if paramKind == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion)
|
||||||
|
if err != nil {
|
||||||
|
// A validatingadmissionpolicy which passes validation should have
|
||||||
|
// a parseable APIVersion for its ParamKind, so this should never happen
|
||||||
|
// if the policy is valid.
|
||||||
|
//
|
||||||
|
// Return a bogus but non-nil GVK that will throw an error about the
|
||||||
|
// invalid APIVersion when the param is looked up.
|
||||||
|
return &schema.GroupVersionKind{
|
||||||
|
Group: paramKind.APIVersion,
|
||||||
|
Version: "",
|
||||||
|
Kind: paramKind.Kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &schema.GroupVersionKind{
|
||||||
|
Group: groupVersion.Group,
|
||||||
|
Version: groupVersion.Version,
|
||||||
|
Kind: paramKind.Kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validatingAdmissionPolicyBindingAccessor struct {
|
||||||
|
*v1beta1.ValidatingAdmissionPolicyBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyBindingAccessor) GetNamespace() string {
|
||||||
|
return v.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyBindingAccessor) GetName() string {
|
||||||
|
return v.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
|
||||||
|
return types.NamespacedName{
|
||||||
|
Namespace: "",
|
||||||
|
Name: v.Spec.PolicyName,
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
@@ -34,47 +32,32 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
utiljson "k8s.io/apimachinery/pkg/util/json"
|
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/warning"
|
"k8s.io/apiserver/pkg/warning"
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ CELPolicyEvaluator = &celAdmissionController{}
|
type dispatcher struct {
|
||||||
|
matcher Matcher
|
||||||
// celAdmissionController is the top-level controller for admission control using CEL
|
|
||||||
// it is responsible for watching policy definitions, bindings, and config param CRDs
|
|
||||||
type celAdmissionController struct {
|
|
||||||
// Controller which manages book-keeping for the cluster's dynamic policy
|
|
||||||
// information.
|
|
||||||
policyController *policyController
|
|
||||||
|
|
||||||
// atomic []policyData
|
|
||||||
// list of every known policy definition, and all informatoin required to
|
|
||||||
// validate its bindings against an object.
|
|
||||||
// A snapshot of the current policy configuration is synced with this field
|
|
||||||
// asynchronously
|
|
||||||
definitions atomic.Value
|
|
||||||
|
|
||||||
authz authorizer.Authorizer
|
authz authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything someone might need to validate a single ValidatingPolicyDefinition
|
var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
|
||||||
// against all of its registered bindings.
|
|
||||||
type policyData struct {
|
func NewDispatcher(
|
||||||
definitionInfo
|
authorizer authorizer.Authorizer,
|
||||||
paramInfo
|
matcher Matcher,
|
||||||
bindings []bindingInfo
|
) generic.Dispatcher[PolicyHook] {
|
||||||
|
return &dispatcher{
|
||||||
|
matcher: matcher,
|
||||||
|
authz: authorizer,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
|
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
|
||||||
@@ -85,110 +68,8 @@ type policyDecisionWithMetadata struct {
|
|||||||
Binding *v1beta1.ValidatingAdmissionPolicyBinding
|
Binding *v1beta1.ValidatingAdmissionPolicyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
// namespaceName is used as a key in definitionInfo and bindingInfos
|
// Dispatch implements generic.Dispatcher.
|
||||||
type namespacedName struct {
|
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
|
||||||
namespace, name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type definitionInfo struct {
|
|
||||||
// Error about the state of the definition's configuration and the cluster
|
|
||||||
// preventing its enforcement or compilation.
|
|
||||||
// Reset every reconciliation
|
|
||||||
configurationError error
|
|
||||||
|
|
||||||
// Last value seen by this controller to be used in policy enforcement
|
|
||||||
// May not be nil
|
|
||||||
lastReconciledValue *v1beta1.ValidatingAdmissionPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
type bindingInfo struct {
|
|
||||||
// Compiled CEL expression turned into an validator
|
|
||||||
validator Validator
|
|
||||||
|
|
||||||
// Last value seen by this controller to be used in policy enforcement
|
|
||||||
// May not be nil
|
|
||||||
lastReconciledValue *v1beta1.ValidatingAdmissionPolicyBinding
|
|
||||||
}
|
|
||||||
|
|
||||||
type paramInfo struct {
|
|
||||||
// Controller which is watching this param CRD
|
|
||||||
controller generic.Controller[runtime.Object]
|
|
||||||
|
|
||||||
// Function to call to stop the informer and clean up the controller
|
|
||||||
stop func()
|
|
||||||
|
|
||||||
// Whether this param is cluster or namespace scoped
|
|
||||||
scope meta.RESTScope
|
|
||||||
|
|
||||||
// Policy Definitions which refer to this param CRD
|
|
||||||
dependentDefinitions sets.Set[namespacedName]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdmissionController(
|
|
||||||
// Injected Dependencies
|
|
||||||
informerFactory informers.SharedInformerFactory,
|
|
||||||
client kubernetes.Interface,
|
|
||||||
restMapper meta.RESTMapper,
|
|
||||||
dynamicClient dynamic.Interface,
|
|
||||||
authz authorizer.Authorizer,
|
|
||||||
) CELPolicyEvaluator {
|
|
||||||
return &celAdmissionController{
|
|
||||||
definitions: atomic.Value{},
|
|
||||||
policyController: newPolicyController(
|
|
||||||
restMapper,
|
|
||||||
client,
|
|
||||||
dynamicClient,
|
|
||||||
informerFactory,
|
|
||||||
nil,
|
|
||||||
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
|
|
||||||
generic.NewInformer[*v1beta1.ValidatingAdmissionPolicy](
|
|
||||||
informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer()),
|
|
||||||
generic.NewInformer[*v1beta1.ValidatingAdmissionPolicyBinding](
|
|
||||||
informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer()),
|
|
||||||
),
|
|
||||||
authz: authz,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
c.policyController.Run(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// Wait indefinitely until policies/bindings are listed & handled before
|
|
||||||
// allowing policies to be refreshed
|
|
||||||
if !cache.WaitForNamedCacheSync("cel-admission-controller", ctx.Done(), c.policyController.HasSynced) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop every 1 second until context is cancelled, refreshing policies
|
|
||||||
wait.Until(c.refreshPolicies, 1*time.Second, ctx.Done())
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-stopCh
|
|
||||||
cancel()
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxAuditAnnotationValueLength = 10 * 1024
|
|
||||||
|
|
||||||
func (c *celAdmissionController) Validate(
|
|
||||||
ctx context.Context,
|
|
||||||
a admission.Attributes,
|
|
||||||
o admission.ObjectInterfaces,
|
|
||||||
) (err error) {
|
|
||||||
if !c.HasSynced() {
|
|
||||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var deniedDecisions []policyDecisionWithMetadata
|
var deniedDecisions []policyDecisionWithMetadata
|
||||||
|
|
||||||
@@ -232,19 +113,18 @@ func (c *celAdmissionController) Validate(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
policyDatas := c.definitions.Load().([]policyData)
|
|
||||||
|
|
||||||
authz := newCachingAuthorizer(c.authz)
|
authz := newCachingAuthorizer(c.authz)
|
||||||
|
|
||||||
for _, definitionInfo := range policyDatas {
|
for _, hook := range hooks {
|
||||||
// versionedAttributes will be set to non-nil inside of the loop, but
|
// versionedAttributes will be set to non-nil inside of the loop, but
|
||||||
// is scoped outside of the param loop so we only convert once. We defer
|
// is scoped outside of the param loop so we only convert once. We defer
|
||||||
// conversion so that it is only performed when we know a policy matches,
|
// conversion so that it is only performed when we know a policy matches,
|
||||||
// saving the cost of converting non-matching requests.
|
// saving the cost of converting non-matching requests.
|
||||||
var versionedAttr *admission.VersionedAttributes
|
var versionedAttr *admission.VersionedAttributes
|
||||||
|
|
||||||
definition := definitionInfo.lastReconciledValue
|
definition := hook.Policy
|
||||||
matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition)
|
matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, definition)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Configuration error.
|
// Configuration error.
|
||||||
addConfigError(err, definition, nil)
|
addConfigError(err, definition, nil)
|
||||||
@@ -253,18 +133,17 @@ func (c *celAdmissionController) Validate(
|
|||||||
if !matches {
|
if !matches {
|
||||||
// Policy definition does not match request
|
// Policy definition does not match request
|
||||||
continue
|
continue
|
||||||
} else if definitionInfo.configurationError != nil {
|
} else if hook.ConfigurationError != nil {
|
||||||
// Configuration error.
|
// Configuration error.
|
||||||
addConfigError(definitionInfo.configurationError, definition, nil)
|
addConfigError(hook.ConfigurationError, definition, nil)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
auditAnnotationCollector := newAuditAnnotationCollector()
|
auditAnnotationCollector := newAuditAnnotationCollector()
|
||||||
for _, bindingInfo := range definitionInfo.bindings {
|
for _, binding := range hook.Bindings {
|
||||||
// If the key is inside dependentBindings, there is guaranteed to
|
// If the key is inside dependentBindings, there is guaranteed to
|
||||||
// be a bindingInfo for it
|
// be a bindingInfo for it
|
||||||
binding := bindingInfo.lastReconciledValue
|
matches, err := c.matcher.BindingMatches(a, o, binding)
|
||||||
matches, err := c.policyController.matcher.BindingMatches(a, o, binding)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Configuration error.
|
// Configuration error.
|
||||||
addConfigError(err, definition, binding)
|
addConfigError(err, definition, binding)
|
||||||
@@ -274,7 +153,14 @@ func (c *celAdmissionController) Validate(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
params, err := c.collectParams(definition.Spec.ParamKind, definitionInfo.paramInfo, binding.Spec.ParamRef, a.GetNamespace())
|
params, err := c.collectParams(
|
||||||
|
definition.Spec.ParamKind,
|
||||||
|
hook.ParamInformer,
|
||||||
|
hook.ParamScope,
|
||||||
|
binding.Spec.ParamRef,
|
||||||
|
a.GetNamespace(),
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
addConfigError(err, definition, binding)
|
addConfigError(err, definition, binding)
|
||||||
continue
|
continue
|
||||||
@@ -303,7 +189,7 @@ func (c *celAdmissionController) Validate(
|
|||||||
// if it is cluster scoped, namespaceName will be empty
|
// if it is cluster scoped, namespaceName will be empty
|
||||||
// Otherwise, get the Namespace resource.
|
// Otherwise, get the Namespace resource.
|
||||||
if namespaceName != "" {
|
if namespaceName != "" {
|
||||||
namespace, err = c.policyController.matcher.GetNamespace(namespaceName)
|
namespace, err = c.matcher.GetNamespace(namespaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -323,7 +209,18 @@ func (c *celAdmissionController) Validate(
|
|||||||
nested: param,
|
nested: param,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
|
|
||||||
|
validationResults = append(validationResults,
|
||||||
|
hook.Evaluator.Validate(
|
||||||
|
ctx,
|
||||||
|
matchResource,
|
||||||
|
versionedAttr,
|
||||||
|
p,
|
||||||
|
namespace,
|
||||||
|
celconfig.RuntimeCELCostBudget,
|
||||||
|
authz,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, validationResult := range validationResults {
|
for _, validationResult := range validationResults {
|
||||||
@@ -344,7 +241,7 @@ func (c *celAdmissionController) Validate(
|
|||||||
})
|
})
|
||||||
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
||||||
case v1beta1.Audit:
|
case v1beta1.Audit:
|
||||||
c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
|
publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
|
||||||
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
||||||
case v1beta1.Warn:
|
case v1beta1.Warn:
|
||||||
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
|
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
|
||||||
@@ -411,28 +308,30 @@ func (c *celAdmissionController) Validate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns objects to use to evaluate the policy
|
// Returns objects to use to evaluate the policy
|
||||||
func (c *celAdmissionController) collectParams(
|
// Copied with minor modification to account for slightly different arguments
|
||||||
|
func (c *dispatcher) collectParams(
|
||||||
paramKind *v1beta1.ParamKind,
|
paramKind *v1beta1.ParamKind,
|
||||||
info paramInfo,
|
paramInformer informers.GenericInformer,
|
||||||
|
paramScope meta.RESTScope,
|
||||||
paramRef *v1beta1.ParamRef,
|
paramRef *v1beta1.ParamRef,
|
||||||
namespace string,
|
namespace string,
|
||||||
) ([]runtime.Object, error) {
|
) ([]runtime.Object, error) {
|
||||||
// If definition has paramKind, paramRef is required in binding.
|
// If definition has paramKind, paramRef is required in binding.
|
||||||
// If definition has no paramKind, paramRef set in binding will be ignored.
|
// If definition has no paramKind, paramRef set in binding will be ignored.
|
||||||
var params []runtime.Object
|
var params []runtime.Object
|
||||||
var paramStore generic.NamespacedLister[runtime.Object]
|
var paramStore cache.GenericNamespaceLister
|
||||||
|
|
||||||
// Make sure the param kind is ready to use
|
// Make sure the param kind is ready to use
|
||||||
if paramKind != nil && paramRef != nil {
|
if paramKind != nil && paramRef != nil {
|
||||||
if info.controller == nil {
|
if paramInformer == nil {
|
||||||
return nil, fmt.Errorf("paramKind kind `%v` not known",
|
return nil, fmt.Errorf("paramKind kind `%v` not known",
|
||||||
paramKind.String())
|
paramKind.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up cluster-scoped, or namespaced access to the params
|
// Set up cluster-scoped, or namespaced access to the params
|
||||||
// "default" if not provided, and paramKind is namespaced
|
// "default" if not provided, and paramKind is namespaced
|
||||||
paramStore = info.controller.Informer()
|
paramStore = paramInformer.Lister()
|
||||||
if info.scope.Name() == meta.RESTScopeNameNamespace {
|
if paramScope.Name() == meta.RESTScopeNameNamespace {
|
||||||
paramsNamespace := namespace
|
paramsNamespace := namespace
|
||||||
if len(paramRef.Namespace) > 0 {
|
if len(paramRef.Namespace) > 0 {
|
||||||
paramsNamespace = paramRef.Namespace
|
paramsNamespace = paramRef.Namespace
|
||||||
@@ -442,16 +341,16 @@ func (c *celAdmissionController) collectParams(
|
|||||||
return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
|
return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
paramStore = info.controller.Informer().Namespaced(paramsNamespace)
|
paramStore = paramInformer.Lister().ByNamespace(paramsNamespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the param informer for this admission policy has not yet
|
// If the param informer for this admission policy has not yet
|
||||||
// had time to perform an initial listing, don't attempt to use
|
// had time to perform an initial listing, don't attempt to use
|
||||||
// it.
|
// it.
|
||||||
timeoutCtx, cancel := context.WithTimeout(c.policyController.context, 1*time.Second)
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if !cache.WaitForCacheSync(timeoutCtx.Done(), info.controller.HasSynced) {
|
if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
|
||||||
return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
|
return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
|
||||||
paramKind.String())
|
paramKind.String())
|
||||||
}
|
}
|
||||||
@@ -467,7 +366,7 @@ func (c *celAdmissionController) collectParams(
|
|||||||
// Policy ParamKind is set, but binding does not use it.
|
// Policy ParamKind is set, but binding does not use it.
|
||||||
// Validate with nil params
|
// Validate with nil params
|
||||||
return []runtime.Object{nil}, nil
|
return []runtime.Object{nil}, nil
|
||||||
case len(paramRef.Namespace) > 0 && info.scope.Name() == meta.RESTScopeRoot.Name():
|
case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name():
|
||||||
// Not allowed to set namespace for cluster-scoped param
|
// Not allowed to set namespace for cluster-scoped param
|
||||||
return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
|
return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
|
||||||
|
|
||||||
@@ -527,10 +426,10 @@ func (c *celAdmissionController) collectParams(
|
|||||||
return params, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
|
func publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
|
||||||
key := "validation.policy.admission.k8s.io/validation_failure"
|
key := "validation.policy.admission.k8s.io/validation_failure"
|
||||||
// Marshal to a list of failures since, in the future, we may need to support multiple failures
|
// Marshal to a list of failures since, in the future, we may need to support multiple failures
|
||||||
valueJson, err := utiljson.Marshal([]validationFailureValue{{
|
valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{
|
||||||
ExpressionIndex: expressionIndex,
|
ExpressionIndex: expressionIndex,
|
||||||
Message: decision.Message,
|
Message: decision.Message,
|
||||||
ValidationActions: binding.Spec.ValidationActions,
|
ValidationActions: binding.Spec.ValidationActions,
|
||||||
@@ -540,27 +439,17 @@ func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
|
klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
|
||||||
}
|
}
|
||||||
value := string(valueJson)
|
value := string(valueJSON)
|
||||||
if err := attributes.AddAnnotation(key, value); err != nil {
|
if err := attributes.AddAnnotation(key, value); err != nil {
|
||||||
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
|
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *celAdmissionController) HasSynced() bool {
|
const maxAuditAnnotationValueLength = 10 * 1024
|
||||||
return c.policyController.HasSynced() && c.definitions.Load() != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionController) ValidateInitialization() error {
|
|
||||||
return c.policyController.matcher.ValidateInitialization()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionController) refreshPolicies() {
|
|
||||||
c.definitions.Store(c.policyController.latestPolicyData())
|
|
||||||
}
|
|
||||||
|
|
||||||
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
|
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
|
||||||
// annotation value.
|
// annotation value.
|
||||||
type validationFailureValue struct {
|
type ValidationFailureValue struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Policy string `json:"policy"`
|
Policy string `json:"policy"`
|
||||||
Binding string `json:"binding"`
|
Binding string `json:"binding"`
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ matching.MatchCriteria = &matchCriteria{}
|
var _ matching.MatchCriteria = &matchCriteria{}
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
celgo "github.com/google/cel-go/cel"
|
celgo "github.com/google/cel-go/cel"
|
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
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 validating
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||||
|
"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"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/client-go/informers"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/component-base/featuregate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginName indicates the name of admission plug-in
|
||||||
|
PluginName = "ValidatingAdmissionPolicy"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
compositionEnvTemplate *cel.CompositionEnv = func() *cel.CompositionEnv {
|
||||||
|
compositionEnvTemplate, err := cel.NewCompositionEnv(cel.VariablesTypeName, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return compositionEnvTemplate
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register registers a plugin
|
||||||
|
func Register(plugins *admission.Plugins) {
|
||||||
|
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
|
||||||
|
return NewPlugin(configFile), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin is an implementation of admission.Interface.
|
||||||
|
type Policy = v1beta1.ValidatingAdmissionPolicy
|
||||||
|
type PolicyBinding = v1beta1.ValidatingAdmissionPolicyBinding
|
||||||
|
type PolicyEvaluator = Validator
|
||||||
|
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
|
||||||
|
|
||||||
|
type Plugin struct {
|
||||||
|
*generic.Plugin[PolicyHook]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ admission.Interface = &Plugin{}
|
||||||
|
var _ admission.ValidationInterface = &Plugin{}
|
||||||
|
var _ initializer.WantsFeatures = &Plugin{}
|
||||||
|
|
||||||
|
func NewPlugin(_ io.Reader) *Plugin {
|
||||||
|
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
|
||||||
|
|
||||||
|
return &Plugin{
|
||||||
|
Plugin: generic.NewPlugin(
|
||||||
|
handler,
|
||||||
|
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
|
||||||
|
return generic.NewPolicySource(
|
||||||
|
f.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer(),
|
||||||
|
f.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer(),
|
||||||
|
NewValidatingAdmissionPolicyAccessor,
|
||||||
|
NewValidatingAdmissionPolicyBindingAccessor,
|
||||||
|
compilePolicy,
|
||||||
|
f,
|
||||||
|
dynamicClient,
|
||||||
|
restMapper,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
|
||||||
|
return NewDispatcher(a, NewMatcher(m))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate makes an admission decision based on the request attributes.
|
||||||
|
func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
||||||
|
return a.Plugin.Dispatch(ctx, attr, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||||
|
a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func compilePolicy(policy *Policy) Validator {
|
||||||
|
hasParam := false
|
||||||
|
if policy.Spec.ParamKind != nil {
|
||||||
|
hasParam = true
|
||||||
|
}
|
||||||
|
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||||
|
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
|
||||||
|
failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policy.Spec.FailurePolicy)
|
||||||
|
var matcher matchconditions.Matcher = nil
|
||||||
|
matchConditions := policy.Spec.MatchConditions
|
||||||
|
|
||||||
|
filterCompiler := cel.NewCompositedCompilerFromTemplate(compositionEnvTemplate)
|
||||||
|
filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions)
|
||||||
|
|
||||||
|
if len(matchConditions) > 0 {
|
||||||
|
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
|
||||||
|
for i := range matchConditions {
|
||||||
|
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||||
|
}
|
||||||
|
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
|
||||||
|
}
|
||||||
|
res := NewValidator(
|
||||||
|
filterCompiler.Compile(convertv1beta1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||||
|
matcher,
|
||||||
|
filterCompiler.Compile(convertv1beta1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||||
|
filterCompiler.Compile(convertv1beta1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||||
|
failurePolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
|
||||||
|
if policyType == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var v1FailPolicy v1.FailurePolicyType
|
||||||
|
if *policyType == v1beta1.Fail {
|
||||||
|
v1FailPolicy = v1.Fail
|
||||||
|
} else if *policyType == v1beta1.Ignore {
|
||||||
|
v1FailPolicy = v1.Ignore
|
||||||
|
}
|
||||||
|
return &v1FailPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
|
||||||
|
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
||||||
|
for i, validation := range inputValidations {
|
||||||
|
validation := ValidationCondition{
|
||||||
|
Expression: validation.Expression,
|
||||||
|
Message: validation.Message,
|
||||||
|
Reason: validation.Reason,
|
||||||
|
}
|
||||||
|
celExpressionAccessor[i] = &validation
|
||||||
|
}
|
||||||
|
return celExpressionAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
|
||||||
|
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
||||||
|
for i, validation := range inputValidations {
|
||||||
|
if validation.MessageExpression != "" {
|
||||||
|
condition := MessageExpressionCondition{
|
||||||
|
MessageExpression: validation.MessageExpression,
|
||||||
|
}
|
||||||
|
celExpressionAccessor[i] = &condition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return celExpressionAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
|
||||||
|
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
||||||
|
for i, validation := range inputValidations {
|
||||||
|
validation := AuditAnnotationCondition{
|
||||||
|
Key: validation.Key,
|
||||||
|
ValueExpression: validation.ValueExpression,
|
||||||
|
}
|
||||||
|
celExpressionAccessor[i] = &validation
|
||||||
|
}
|
||||||
|
return celExpressionAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertv1beta1Variables(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
|
||||||
|
}
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -26,11 +26,35 @@ import (
|
|||||||
appsv1 "k8s.io/api/apps/v1"
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scheme *runtime.Scheme = func() *runtime.Scheme {
|
||||||
|
res := runtime.NewScheme()
|
||||||
|
if err := v1beta1.AddToScheme(res); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fake.AddToScheme(res); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
|
func must3[T any, I any](val T, _ I, err error) T {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractTypeNames(t *testing.T) {
|
func TestExtractTypeNames(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package validatingadmissionpolicy
|
package validating
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@@ -1,197 +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 validatingadmissionpolicy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
||||||
"k8s.io/apiserver/pkg/features"
|
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
"k8s.io/component-base/featuregate"
|
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/admission"
|
|
||||||
"k8s.io/apiserver/pkg/admission/initializer"
|
|
||||||
"k8s.io/client-go/informers"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
)
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Plugin Definition
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Definition for CEL admission plugin. This is the entry point into the
|
|
||||||
// CEL admission control system.
|
|
||||||
//
|
|
||||||
// Each plugin is asked to validate every object update.
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PluginName indicates the name of admission plug-in
|
|
||||||
PluginName = "ValidatingAdmissionPolicy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Register registers a plugin
|
|
||||||
func Register(plugins *admission.Plugins) {
|
|
||||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
|
||||||
return NewPlugin()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Plugin Initialization & Dependency Injection
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
type celAdmissionPlugin struct {
|
|
||||||
*admission.Handler
|
|
||||||
evaluator CELPolicyEvaluator
|
|
||||||
|
|
||||||
inspectedFeatureGates bool
|
|
||||||
enabled bool
|
|
||||||
|
|
||||||
// Injected Dependencies
|
|
||||||
informerFactory informers.SharedInformerFactory
|
|
||||||
client kubernetes.Interface
|
|
||||||
restMapper meta.RESTMapper
|
|
||||||
dynamicClient dynamic.Interface
|
|
||||||
stopCh <-chan struct{}
|
|
||||||
authorizer authorizer.Authorizer
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
|
|
||||||
var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
|
|
||||||
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
|
|
||||||
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
|
|
||||||
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
|
|
||||||
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
|
|
||||||
var _ admission.InitializationValidator = &celAdmissionPlugin{}
|
|
||||||
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
|
||||||
|
|
||||||
func NewPlugin() (admission.Interface, error) {
|
|
||||||
return &celAdmissionPlugin{
|
|
||||||
Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
|
||||||
c.informerFactory = f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
|
|
||||||
c.client = client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
|
|
||||||
c.restMapper = mapper
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) {
|
|
||||||
c.dynamicClient = client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
|
|
||||||
c.stopCh = stopCh
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
|
|
||||||
c.authorizer = authorizer
|
|
||||||
}
|
|
||||||
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
|
||||||
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
|
|
||||||
c.enabled = true
|
|
||||||
}
|
|
||||||
c.inspectedFeatureGates = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
|
|
||||||
func (c *celAdmissionPlugin) ValidateInitialization() error {
|
|
||||||
if !c.inspectedFeatureGates {
|
|
||||||
return fmt.Errorf("%s did not see feature gates", PluginName)
|
|
||||||
}
|
|
||||||
if !c.enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
|
|
||||||
if err := c.evaluator.ValidateInitialization(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetReadyFunc(c.evaluator.HasSynced)
|
|
||||||
go c.evaluator.Run(c.stopCh)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// admission.ValidationInterface
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) Handles(operation admission.Operation) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) Validate(
|
|
||||||
ctx context.Context,
|
|
||||||
a admission.Attributes,
|
|
||||||
o admission.ObjectInterfaces,
|
|
||||||
) (err error) {
|
|
||||||
if !c.enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPolicyResource determines if an admission.Attributes object is describing
|
|
||||||
// the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
|
|
||||||
if isPolicyResource(a) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.WaitForReady() {
|
|
||||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.evaluator.Validate(ctx, a, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPolicyResource(attr admission.Attributes) bool {
|
|
||||||
gvk := attr.GetResource()
|
|
||||||
if gvk.Group == "admissionregistration.k8s.io" {
|
|
||||||
if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@@ -1,551 +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 validatingadmissionpolicy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
|
||||||
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
|
||||||
"k8s.io/apiserver/pkg/cel/environment"
|
|
||||||
"k8s.io/client-go/dynamic"
|
|
||||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
|
||||||
"k8s.io/client-go/informers"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
"k8s.io/client-go/tools/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
type policyController struct {
|
|
||||||
once sync.Once
|
|
||||||
context context.Context
|
|
||||||
dynamicClient dynamic.Interface
|
|
||||||
informerFactory informers.SharedInformerFactory
|
|
||||||
restMapper meta.RESTMapper
|
|
||||||
policyDefinitionsController generic.Controller[*v1beta1.ValidatingAdmissionPolicy]
|
|
||||||
policyBindingController generic.Controller[*v1beta1.ValidatingAdmissionPolicyBinding]
|
|
||||||
|
|
||||||
// Provided to the policy's Compile function as an injected dependency to
|
|
||||||
// assist with compiling its expressions to CEL
|
|
||||||
// pass nil to create filter compiler in demand
|
|
||||||
filterCompiler cel.FilterCompiler
|
|
||||||
|
|
||||||
matcher Matcher
|
|
||||||
|
|
||||||
newValidator
|
|
||||||
|
|
||||||
client kubernetes.Interface
|
|
||||||
// Lock which protects
|
|
||||||
// All Below fields
|
|
||||||
// All above fields should be assumed constant
|
|
||||||
mutex sync.RWMutex
|
|
||||||
|
|
||||||
cachedPolicies []policyData
|
|
||||||
|
|
||||||
// controller and metadata
|
|
||||||
paramsCRDControllers map[v1beta1.ParamKind]*paramInfo
|
|
||||||
|
|
||||||
// Index for each definition namespace/name, contains all binding
|
|
||||||
// namespace/names known to exist for that definition
|
|
||||||
definitionInfo map[namespacedName]*definitionInfo
|
|
||||||
|
|
||||||
// Index for each bindings namespace/name. Contains compiled templates
|
|
||||||
// for the binding depending on the policy/param combination.
|
|
||||||
bindingInfos map[namespacedName]*bindingInfo
|
|
||||||
|
|
||||||
// Map from namespace/name of a definition to a set of namespace/name
|
|
||||||
// of bindings which depend on it.
|
|
||||||
// All keys must have at least one dependent binding
|
|
||||||
// All binding names MUST exist as a key bindingInfos
|
|
||||||
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
|
||||||
}
|
|
||||||
|
|
||||||
type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType) Validator
|
|
||||||
|
|
||||||
func newPolicyController(
|
|
||||||
restMapper meta.RESTMapper,
|
|
||||||
client kubernetes.Interface,
|
|
||||||
dynamicClient dynamic.Interface,
|
|
||||||
informerFactory informers.SharedInformerFactory,
|
|
||||||
filterCompiler cel.FilterCompiler,
|
|
||||||
matcher Matcher,
|
|
||||||
policiesInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicy],
|
|
||||||
bindingsInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicyBinding],
|
|
||||||
) *policyController {
|
|
||||||
res := &policyController{}
|
|
||||||
*res = policyController{
|
|
||||||
filterCompiler: filterCompiler,
|
|
||||||
definitionInfo: make(map[namespacedName]*definitionInfo),
|
|
||||||
bindingInfos: make(map[namespacedName]*bindingInfo),
|
|
||||||
paramsCRDControllers: make(map[v1beta1.ParamKind]*paramInfo),
|
|
||||||
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
|
|
||||||
matcher: matcher,
|
|
||||||
newValidator: NewValidator,
|
|
||||||
policyDefinitionsController: generic.NewController(
|
|
||||||
policiesInformer,
|
|
||||||
res.reconcilePolicyDefinition,
|
|
||||||
generic.ControllerOptions{
|
|
||||||
Workers: 1,
|
|
||||||
Name: "cel-policy-definitions",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
policyBindingController: generic.NewController(
|
|
||||||
bindingsInformer,
|
|
||||||
res.reconcilePolicyBinding,
|
|
||||||
generic.ControllerOptions{
|
|
||||||
Workers: 1,
|
|
||||||
Name: "cel-policy-bindings",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
restMapper: restMapper,
|
|
||||||
dynamicClient: dynamicClient,
|
|
||||||
informerFactory: informerFactory,
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) Run(ctx context.Context) {
|
|
||||||
// Only support being run once
|
|
||||||
c.once.Do(func() {
|
|
||||||
c.context = ctx
|
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
c.policyDefinitionsController.Run(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
c.policyBindingController.Run(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
wg.Wait()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) HasSynced() bool {
|
|
||||||
return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
err := c.reconcilePolicyDefinitionSpec(namespace, name, definition)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) reconcilePolicyDefinitionSpec(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
|
|
||||||
c.cachedPolicies = nil // invalidate cachedPolicies
|
|
||||||
|
|
||||||
// Namespace for policydefinition is empty.
|
|
||||||
nn := getNamespaceName(namespace, name)
|
|
||||||
info, ok := c.definitionInfo[nn]
|
|
||||||
if !ok {
|
|
||||||
info = &definitionInfo{}
|
|
||||||
c.definitionInfo[nn] = info
|
|
||||||
// TODO(DangerOnTheRanger): add support for "warn" being a valid enforcementAction
|
|
||||||
celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip reconcile if the spec of the definition is unchanged and had a
|
|
||||||
// successful previous sync
|
|
||||||
if info.configurationError == nil && info.lastReconciledValue != nil && definition != nil &&
|
|
||||||
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, definition.Spec) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var paramSource *v1beta1.ParamKind
|
|
||||||
if definition != nil {
|
|
||||||
paramSource = definition.Spec.ParamKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// If param source has changed, remove definition as dependent of old params
|
|
||||||
// If there are no more dependents of old param, stop and clean up controller
|
|
||||||
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
|
|
||||||
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
|
|
||||||
|
|
||||||
// If we are:
|
|
||||||
// - switching from having a param to not having a param (includes deletion)
|
|
||||||
// - or from having a param to a different one
|
|
||||||
// we remove dependency on the controller.
|
|
||||||
if paramSource == nil || *paramSource != oldParamSource {
|
|
||||||
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
|
|
||||||
oldParamInfo.dependentDefinitions.Delete(nn)
|
|
||||||
if len(oldParamInfo.dependentDefinitions) == 0 {
|
|
||||||
oldParamInfo.stop()
|
|
||||||
delete(c.paramsCRDControllers, oldParamSource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset all previously compiled evaluators in case something relevant in
|
|
||||||
// definition has changed.
|
|
||||||
for key := range c.definitionsToBindings[nn] {
|
|
||||||
bindingInfo := c.bindingInfos[key]
|
|
||||||
bindingInfo.validator = nil
|
|
||||||
c.bindingInfos[key] = bindingInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
if definition == nil {
|
|
||||||
delete(c.definitionInfo, nn)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update definition info
|
|
||||||
info.lastReconciledValue = definition
|
|
||||||
info.configurationError = nil
|
|
||||||
|
|
||||||
if paramSource == nil {
|
|
||||||
// Skip setting up controller for empty param type
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// find GVR for params
|
|
||||||
// Parse param source into a GVK
|
|
||||||
|
|
||||||
paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
|
|
||||||
if err != nil {
|
|
||||||
// Failed to resolve. Return error so we retry again (rate limited)
|
|
||||||
// Save a record of this definition with an evaluator that unconditionally
|
|
||||||
info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
|
|
||||||
|
|
||||||
// Return nil, since this error cannot be resolved by waiting more time
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
|
|
||||||
Group: paramSourceGV.Group,
|
|
||||||
Kind: paramSource.Kind,
|
|
||||||
}, paramSourceGV.Version)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// Failed to resolve. Return error so we retry again (rate limited)
|
|
||||||
// Save a record of this definition with an evaluator that unconditionally
|
|
||||||
//
|
|
||||||
info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
|
|
||||||
return info.configurationError
|
|
||||||
}
|
|
||||||
|
|
||||||
paramInfo := c.ensureParamInfo(paramSource, paramsGVR)
|
|
||||||
paramInfo.dependentDefinitions.Insert(nn)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures that there is an informer started for the given GVK to be used as a
|
|
||||||
// param
|
|
||||||
func (c *policyController) ensureParamInfo(paramSource *v1beta1.ParamKind, mapping *meta.RESTMapping) *paramInfo {
|
|
||||||
if info, ok := c.paramsCRDControllers[*paramSource]; ok {
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are not watching this param. Start an informer for it.
|
|
||||||
instanceContext, instanceCancel := context.WithCancel(c.context)
|
|
||||||
|
|
||||||
var informer cache.SharedIndexInformer
|
|
||||||
|
|
||||||
// Try to see if our provided informer factory has an informer for this type.
|
|
||||||
// We assume the informer is already started, and starts all types associated
|
|
||||||
// with it.
|
|
||||||
if genericInformer, err := c.informerFactory.ForResource(mapping.Resource); err == nil {
|
|
||||||
informer = genericInformer.Informer()
|
|
||||||
|
|
||||||
// Ensure the informer is started
|
|
||||||
// Use policyController's context rather than the instance context.
|
|
||||||
// PolicyController context is expected to last until app shutdown
|
|
||||||
// This is due to behavior of informerFactory which would cause the
|
|
||||||
// informer to stop running once the context is cancelled, and
|
|
||||||
// never started again.
|
|
||||||
c.informerFactory.Start(c.context.Done())
|
|
||||||
} else {
|
|
||||||
// Dynamic JSON informer fallback.
|
|
||||||
// Cannot use shared dynamic informer since it would be impossible
|
|
||||||
// to clean CRD informers properly with multiple dependents
|
|
||||||
// (cannot start ahead of time, and cannot track dependencies via stopCh)
|
|
||||||
informer = dynamicinformer.NewFilteredDynamicInformer(
|
|
||||||
c.dynamicClient,
|
|
||||||
mapping.Resource,
|
|
||||||
corev1.NamespaceAll,
|
|
||||||
// Use same interval as is used for k8s typed sharedInformerFactory
|
|
||||||
// https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430
|
|
||||||
10*time.Minute,
|
|
||||||
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
|
|
||||||
nil,
|
|
||||||
).Informer()
|
|
||||||
go informer.Run(instanceContext.Done())
|
|
||||||
}
|
|
||||||
|
|
||||||
controller := generic.NewController(
|
|
||||||
generic.NewInformer[runtime.Object](informer),
|
|
||||||
c.reconcileParams,
|
|
||||||
generic.ControllerOptions{
|
|
||||||
Workers: 1,
|
|
||||||
Name: paramSource.String() + "-controller",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ret := ¶mInfo{
|
|
||||||
controller: controller,
|
|
||||||
stop: instanceCancel,
|
|
||||||
scope: mapping.Scope,
|
|
||||||
dependentDefinitions: sets.New[namespacedName](),
|
|
||||||
}
|
|
||||||
c.paramsCRDControllers[*paramSource] = ret
|
|
||||||
|
|
||||||
go controller.Run(instanceContext)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1beta1.ValidatingAdmissionPolicyBinding) error {
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
c.cachedPolicies = nil // invalidate cachedPolicies
|
|
||||||
|
|
||||||
// Namespace for PolicyBinding is empty. In the future a namespaced binding
|
|
||||||
// may be added
|
|
||||||
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
|
|
||||||
nn := getNamespaceName(namespace, name)
|
|
||||||
info, ok := c.bindingInfos[nn]
|
|
||||||
if !ok {
|
|
||||||
info = &bindingInfo{}
|
|
||||||
c.bindingInfos[nn] = info
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if the spec of the binding is unchanged.
|
|
||||||
if info.lastReconciledValue != nil && binding != nil &&
|
|
||||||
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, binding.Spec) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldNamespacedDefinitionName namespacedName
|
|
||||||
if info.lastReconciledValue != nil {
|
|
||||||
// All validating policies are cluster-scoped so have empty namespace
|
|
||||||
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var namespacedDefinitionName namespacedName
|
|
||||||
if binding != nil {
|
|
||||||
// All validating policies are cluster-scoped so have empty namespace
|
|
||||||
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove record of binding from old definition if the referred policy
|
|
||||||
// has changed
|
|
||||||
if oldNamespacedDefinitionName != namespacedDefinitionName {
|
|
||||||
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
|
|
||||||
dependentBindings.Delete(nn)
|
|
||||||
|
|
||||||
// if there are no more dependent bindings, remove knowledge of the
|
|
||||||
// definition altogether
|
|
||||||
if len(dependentBindings) == 0 {
|
|
||||||
delete(c.definitionsToBindings, oldNamespacedDefinitionName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if binding == nil {
|
|
||||||
delete(c.bindingInfos, nn)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add record of binding to new definition
|
|
||||||
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
|
|
||||||
dependentBindings.Insert(nn)
|
|
||||||
} else {
|
|
||||||
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove compiled template for old binding
|
|
||||||
info.validator = nil
|
|
||||||
info.lastReconciledValue = binding
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error {
|
|
||||||
// Do nothing.
|
|
||||||
// When we add informational type checking we will need to compile in the
|
|
||||||
// reconcile loops instead of lazily so we can add compiler errors / type
|
|
||||||
// checker errors to the status of the resources.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetches the latest set of policy data or recalculates it if it has changed
|
|
||||||
// since it was last fetched
|
|
||||||
func (c *policyController) latestPolicyData() []policyData {
|
|
||||||
existing := func() []policyData {
|
|
||||||
c.mutex.RLock()
|
|
||||||
defer c.mutex.RUnlock()
|
|
||||||
|
|
||||||
return c.cachedPolicies
|
|
||||||
}()
|
|
||||||
|
|
||||||
if existing != nil {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mutex.Lock()
|
|
||||||
defer c.mutex.Unlock()
|
|
||||||
|
|
||||||
var res []policyData
|
|
||||||
for definitionNN, definitionInfo := range c.definitionInfo {
|
|
||||||
var bindingInfos []bindingInfo
|
|
||||||
for bindingNN := range c.definitionsToBindings[definitionNN] {
|
|
||||||
bindingInfo := c.bindingInfos[bindingNN]
|
|
||||||
if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
|
|
||||||
hasParam := false
|
|
||||||
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
|
|
||||||
hasParam = true
|
|
||||||
}
|
|
||||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
|
||||||
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
|
|
||||||
failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy)
|
|
||||||
var matcher matchconditions.Matcher = nil
|
|
||||||
matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions
|
|
||||||
|
|
||||||
filterCompiler := c.filterCompiler
|
|
||||||
if filterCompiler == nil {
|
|
||||||
compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
|
||||||
if err == nil {
|
|
||||||
filterCompiler = compositedCompiler
|
|
||||||
compositedCompiler.CompileAndStoreVariables(convertv1beta1Variables(definitionInfo.lastReconciledValue.Spec.Variables), optionalVars, environment.StoredExpressions)
|
|
||||||
} else {
|
|
||||||
utilruntime.HandleError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(matchConditions) > 0 {
|
|
||||||
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
|
|
||||||
for i := range matchConditions {
|
|
||||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
|
||||||
}
|
|
||||||
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", definitionInfo.lastReconciledValue.Name)
|
|
||||||
}
|
|
||||||
bindingInfo.validator = c.newValidator(
|
|
||||||
filterCompiler.Compile(convertv1beta1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions),
|
|
||||||
matcher,
|
|
||||||
filterCompiler.Compile(convertv1beta1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
|
||||||
filterCompiler.Compile(convertv1beta1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
|
||||||
failurePolicy,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
bindingInfos = append(bindingInfos, *bindingInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
var pInfo paramInfo
|
|
||||||
if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil {
|
|
||||||
if info, ok := c.paramsCRDControllers[*paramKind]; ok {
|
|
||||||
pInfo = *info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, policyData{
|
|
||||||
definitionInfo: *definitionInfo,
|
|
||||||
paramInfo: pInfo,
|
|
||||||
bindings: bindingInfos,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cachedPolicies = res
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
|
|
||||||
if policyType == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var v1FailPolicy v1.FailurePolicyType
|
|
||||||
if *policyType == v1beta1.Fail {
|
|
||||||
v1FailPolicy = v1.Fail
|
|
||||||
} else if *policyType == v1beta1.Ignore {
|
|
||||||
v1FailPolicy = v1.Ignore
|
|
||||||
}
|
|
||||||
return &v1FailPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
|
|
||||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
|
||||||
for i, validation := range inputValidations {
|
|
||||||
validation := ValidationCondition{
|
|
||||||
Expression: validation.Expression,
|
|
||||||
Message: validation.Message,
|
|
||||||
Reason: validation.Reason,
|
|
||||||
}
|
|
||||||
celExpressionAccessor[i] = &validation
|
|
||||||
}
|
|
||||||
return celExpressionAccessor
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
|
|
||||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
|
||||||
for i, validation := range inputValidations {
|
|
||||||
if validation.MessageExpression != "" {
|
|
||||||
condition := MessageExpressionCondition{
|
|
||||||
MessageExpression: validation.MessageExpression,
|
|
||||||
}
|
|
||||||
celExpressionAccessor[i] = &condition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return celExpressionAccessor
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
|
|
||||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
|
||||||
for i, validation := range inputValidations {
|
|
||||||
validation := AuditAnnotationCondition{
|
|
||||||
Key: validation.Key,
|
|
||||||
ValueExpression: validation.ValueExpression,
|
|
||||||
}
|
|
||||||
celExpressionAccessor[i] = &validation
|
|
||||||
}
|
|
||||||
return celExpressionAccessor
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertv1beta1Variables(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
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNamespaceName(namespace, name string) namespacedName {
|
|
||||||
return namespacedName{
|
|
||||||
namespace: namespace,
|
|
||||||
name: name,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -29,7 +29,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/admission/initializer"
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||||
apiserverapi "k8s.io/apiserver/pkg/apis/apiserver"
|
apiserverapi "k8s.io/apiserver/pkg/apis/apiserver"
|
||||||
|
@@ -20,7 +20,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||||
)
|
)
|
||||||
|
9
vendor/modules.txt
vendored
9
vendor/modules.txt
vendored
@@ -1360,12 +1360,16 @@ k8s.io/apimachinery/third_party/forked/golang/reflect
|
|||||||
# k8s.io/apiserver v0.0.0 => ./staging/src/k8s.io/apiserver
|
# k8s.io/apiserver v0.0.0 => ./staging/src/k8s.io/apiserver
|
||||||
## explicit; go 1.21
|
## explicit; go 1.21
|
||||||
k8s.io/apiserver/pkg/admission
|
k8s.io/apiserver/pkg/admission
|
||||||
k8s.io/apiserver/pkg/admission/cel
|
|
||||||
k8s.io/apiserver/pkg/admission/configuration
|
k8s.io/apiserver/pkg/admission/configuration
|
||||||
k8s.io/apiserver/pkg/admission/initializer
|
k8s.io/apiserver/pkg/admission/initializer
|
||||||
k8s.io/apiserver/pkg/admission/metrics
|
k8s.io/apiserver/pkg/admission/metrics
|
||||||
k8s.io/apiserver/pkg/admission/plugin/cel
|
k8s.io/apiserver/pkg/admission/plugin/cel
|
||||||
k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
|
k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
|
||||||
|
k8s.io/apiserver/pkg/admission/plugin/policy/generic
|
||||||
|
k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic
|
||||||
|
k8s.io/apiserver/pkg/admission/plugin/policy/matching
|
||||||
|
k8s.io/apiserver/pkg/admission/plugin/policy/validating
|
||||||
|
k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics
|
||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota
|
||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota
|
||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/install
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/install
|
||||||
@@ -1373,9 +1377,6 @@ k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1
|
|||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1
|
||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1
|
||||||
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/validation
|
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/validation
|
||||||
k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy
|
|
||||||
k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic
|
|
||||||
k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching
|
|
||||||
k8s.io/apiserver/pkg/admission/plugin/webhook
|
k8s.io/apiserver/pkg/admission/plugin/webhook
|
||||||
k8s.io/apiserver/pkg/admission/plugin/webhook/config
|
k8s.io/apiserver/pkg/admission/plugin/webhook/config
|
||||||
k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission
|
k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission
|
||||||
|
Reference in New Issue
Block a user