rebase: update kubernetes to 1.30

updating kubernetes to 1.30 release

Signed-off-by: Madhu Rajanna <madhupr007@gmail.com>
This commit is contained in:
Madhu Rajanna
2024-05-15 08:54:18 +02:00
committed by mergify[bot]
parent 62ddcf715b
commit e727bd351e
747 changed files with 73809 additions and 10436 deletions

View File

@ -0,0 +1,42 @@
/*
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/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/types"
)
type PolicyAccessor interface {
GetName() string
GetNamespace() string
GetParamKind() *v1.ParamKind
GetMatchConstraints() *v1.MatchResources
}
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
GetParamRef() *v1.ParamRef
GetMatchResources() *v1.MatchResources
}

View File

@ -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 {
}

View File

@ -0,0 +1,215 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package generic
import (
"context"
"errors"
"fmt"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
// H is the Hook type generated by the source and consumed by the dispatcher.
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
// admissionResources is the list of resources related to CEL-based admission
// features.
var admissionResources = []schema.GroupResource{
{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicies"},
{Group: admissionregistrationv1.GroupName, Resource: "validatingadmissionpolicybindings"},
{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicies"},
{Group: admissionregistrationv1.GroupName, Resource: "mutatingadmissionpolicybindings"},
}
// AdmissionPolicyManager is an abstract admission plugin with all the
// infrastructure to define Admit or Validate on-top.
type Plugin[H any] struct {
*admission.Handler
sourceFactory sourceFactory[H]
dispatcherFactory dispatcherFactory[H]
source Source[H]
dispatcher Dispatcher[H]
matcher *matching.Matcher
informerFactory informers.SharedInformerFactory
client kubernetes.Interface
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
excludedResources sets.Set[schema.GroupResource]
stopCh <-chan struct{}
authorizer authorizer.Authorizer
enabled bool
}
var (
_ initializer.WantsExternalKubeInformerFactory = &Plugin[any]{}
_ initializer.WantsExternalKubeClientSet = &Plugin[any]{}
_ initializer.WantsRESTMapper = &Plugin[any]{}
_ initializer.WantsDynamicClient = &Plugin[any]{}
_ initializer.WantsDrainedNotification = &Plugin[any]{}
_ initializer.WantsAuthorizer = &Plugin[any]{}
_ initializer.WantsExcludedAdmissionResources = &Plugin[any]{}
_ admission.InitializationValidator = &Plugin[any]{}
)
func NewPlugin[H any](
handler *admission.Handler,
sourceFactory sourceFactory[H],
dispatcherFactory dispatcherFactory[H],
) *Plugin[H] {
return &Plugin[H]{
Handler: handler,
sourceFactory: sourceFactory,
dispatcherFactory: dispatcherFactory,
// always exclude admission/mutating policies and bindings
excludedResources: sets.New(admissionResources...),
}
}
func (c *Plugin[H]) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.informerFactory = f
}
func (c *Plugin[H]) SetExternalKubeClientSet(client kubernetes.Interface) {
c.client = client
}
func (c *Plugin[H]) SetRESTMapper(mapper meta.RESTMapper) {
c.restMapper = mapper
}
func (c *Plugin[H]) SetDynamicClient(client dynamic.Interface) {
c.dynamicClient = client
}
func (c *Plugin[H]) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *Plugin[H]) SetAuthorizer(authorizer authorizer.Authorizer) {
c.authorizer = authorizer
}
func (c *Plugin[H]) SetMatcher(matcher *matching.Matcher) {
c.matcher = matcher
}
func (c *Plugin[H]) SetEnabled(enabled bool) {
c.enabled = enabled
}
func (c *Plugin[H]) SetExcludedAdmissionResources(excludedResources []schema.GroupResource) {
c.excludedResources.Insert(excludedResources...)
}
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *Plugin[H]) ValidateInitialization() error {
// By default enabled is set to false. It is up to types which embed this
// struct to set it to true (if feature gate is enabled, or other conditions)
if !c.enabled {
return nil
}
if c.Handler == nil {
return errors.New("missing handler")
}
if c.informerFactory == nil {
return errors.New("missing informer factory")
}
if c.client == nil {
return errors.New("missing kubernetes client")
}
if c.restMapper == nil {
return errors.New("missing rest mapper")
}
if c.dynamicClient == nil {
return errors.New("missing dynamic client")
}
if c.stopCh == nil {
return errors.New("missing stop channel")
}
if c.authorizer == nil {
return errors.New("missing authorizer")
}
// Use default matcher
namespaceInformer := c.informerFactory.Core().V1().Namespaces()
c.matcher = matching.NewMatcher(namespaceInformer.Lister(), c.client)
if err := c.matcher.ValidateInitialization(); err != nil {
return err
}
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
go func() {
defer pluginContextCancel()
<-c.stopCh
}()
go func() {
err := c.source.Run(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
}
}()
c.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
})
return nil
}
func (c *Plugin[H]) Dispatch(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.enabled {
return nil
} else if c.shouldIgnoreResource(a) {
return nil
} else if !c.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
return c.dispatcher.Dispatch(ctx, a, o, c.source.Hooks())
}
func (c *Plugin[H]) shouldIgnoreResource(attr admission.Attributes) bool {
gvr := attr.GetResource()
// exclusion decision ignores the version.
gr := gvr.GroupResource()
return c.excludedResources.Has(gr)
}

View File

@ -0,0 +1,354 @@
/*
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"
"time"
"k8s.io/api/admissionregistration/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
// in the context of a specific request. The params have already been resolved
// and any error in configuration or setting up the invocation is stored in
// the Error field.
type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
// Relevant policy for this hook.
// This field is always populated
Policy P
// Matched Kind for the request given the policy's matchconstraints
// May be empty if there was an error matching the resource
Kind schema.GroupVersionKind
// Matched Resource for the request given the policy's matchconstraints
// May be empty if there was an error matching the resource
Resource schema.GroupVersionResource
// Relevant binding for this hook.
// May be empty if there was an error with the policy's configuration itself
Binding B
// Compiled policy evaluator
Evaluator E
// Params fetched by the binding to use to evaluate the policy
Param runtime.Object
// Error is set if there was an error with the policy or binding or its
// params, etc
Error error
}
// dispatcherDelegate is called during a request with a pre-filtered list
// of (Policy, Binding, Param) tuples that are active and match the request.
// The dispatcher delegate is responsible for updating the object on the
// admission attributes in the case of mutation, or returning a status error in
// the case of validation.
//
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
newPolicyAccessor func(P) PolicyAccessor
newBindingAccessor func(B) BindingAccessor
matcher PolicyMatcher
delegate dispatcherDelegate[P, B, E]
}
func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
newPolicyAccessor func(P) PolicyAccessor,
newBindingAccessor func(B) BindingAccessor,
matcher *matching.Matcher,
delegate dispatcherDelegate[P, B, E],
) Dispatcher[PolicyHook[P, B, E]] {
return &policyDispatcher[P, B, E]{
newPolicyAccessor: newPolicyAccessor,
newBindingAccessor: newBindingAccessor,
matcher: NewPolicyMatcher(matcher),
delegate: delegate,
}
}
// Dispatch implements generic.Dispatcher. It loops through all active hooks
// (policy x binding pairs) and selects those which are active for the current
// request. It then resolves all params and creates an Invocation for each
// matching policy-binding-param tuple. The delegate is then called with the
// list of tuples.
//
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
// is expected to ignore the result of any policies whose match conditions dont pass.
// This may be possible to refactor so matchconditions are checked here instead.
func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook[P, B, E]) error {
var relevantHooks []PolicyInvocation[P, B, E]
// Construct all the versions we need to call our webhooks
versionedAttrAccessor := &versionedAttributeAccessor{
versionedAttrs: map[schema.GroupVersionKind]*admission.VersionedAttributes{},
attr: a,
objectInterfaces: o,
}
for _, hook := range hooks {
policyAccessor := d.newPolicyAccessor(hook.Policy)
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
if err != nil {
// There was an error evaluating if this policy matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: err,
})
continue
} else if !matches {
continue
} else if hook.ConfigurationError != nil {
// The policy matches but there is a configuration error with the
// policy itself
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: hook.ConfigurationError,
Resource: matchGVR,
Kind: matchGVK,
})
utilruntime.HandleError(hook.ConfigurationError)
continue
}
for _, binding := range hook.Bindings {
bindingAccessor := d.newBindingAccessor(binding)
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
if err != nil {
// There was an error evaluating if this binding matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
continue
} else if !matches {
continue
}
// Collect params for this binding
params, err := CollectParams(
policyAccessor.GetParamKind(),
hook.ParamInformer,
hook.ParamScope,
bindingAccessor.GetParamRef(),
a.GetNamespace(),
)
if err != nil {
// There was an error collecting params for this binding.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
continue
}
// If params is empty and there was no error, that means that
// ParamNotFoundAction is ignore, so it shouldnt be added to list
for _, param := range params {
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Kind: matchGVK,
Resource: matchGVR,
Param: param,
Evaluator: hook.Evaluator,
})
}
// VersionedAttr result will be cached and reused later during parallel
// hook calls
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
if err != nil {
return apierrors.NewInternalError(err)
}
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
}
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
}
// Returns params to use to evaluate a policy-binding with given param
// configuration. If the policy-binding has no param configuration, it
// returns a single-element list with a nil param.
func CollectParams(
paramKind *v1.ParamKind,
paramInformer informers.GenericInformer,
paramScope meta.RESTScope,
paramRef *v1.ParamRef,
namespace string,
) ([]runtime.Object, error) {
// If definition has paramKind, paramRef is required in binding.
// If definition has no paramKind, paramRef set in binding will be ignored.
var params []runtime.Object
var paramStore cache.GenericNamespaceLister
// Make sure the param kind is ready to use
if paramKind != nil && paramRef != nil {
if paramInformer == nil {
return nil, fmt.Errorf("paramKind kind `%v` not known",
paramKind.String())
}
// Set up cluster-scoped, or namespaced access to the params
// "default" if not provided, and paramKind is namespaced
paramStore = paramInformer.Lister()
if paramScope.Name() == meta.RESTScopeNameNamespace {
paramsNamespace := namespace
if len(paramRef.Namespace) > 0 {
paramsNamespace = paramRef.Namespace
} else if len(paramsNamespace) == 0 {
// You must supply namespace if your matcher can possibly
// match a cluster-scoped resource
return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
}
paramStore = paramInformer.Lister().ByNamespace(paramsNamespace)
}
// If the param informer for this admission policy has not yet
// had time to perform an initial listing, don't attempt to use
// it.
timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String())
}
}
// Find params to use with policy
switch {
case paramKind == nil:
// ParamKind is unset. Ignore any globalParamRef or namespaceParamRef
// setting.
return []runtime.Object{nil}, nil
case paramRef == nil:
// Policy ParamKind is set, but binding does not use it.
// Validate with nil params
return []runtime.Object{nil}, nil
case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name():
// Not allowed to set namespace for cluster-scoped param
return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
case len(paramRef.Name) > 0:
if paramRef.Selector != nil {
// This should be validated, but just in case.
return nil, fmt.Errorf("paramRef.name and paramRef.selector are mutually exclusive")
}
switch param, err := paramStore.Get(paramRef.Name); {
case err == nil:
params = []runtime.Object{param}
case apierrors.IsNotFound(err):
// Param not yet available. User may need to wait a bit
// before being able to use it for validation.
//
// Set params to nil to prepare for not found action
params = nil
case apierrors.IsInvalid(err):
// Param mis-configured
// require to set namespace for namespaced resource
// and unset namespace for cluster scoped resource
return nil, err
default:
// Internal error
utilruntime.HandleError(err)
return nil, err
}
case paramRef.Selector != nil:
// Select everything by default if empty name and selector
selector, err := metav1.LabelSelectorAsSelector(paramRef.Selector)
if err != nil {
// Cannot parse label selector: configuration error
return nil, err
}
paramList, err := paramStore.List(selector)
if err != nil {
// There was a bad internal error
utilruntime.HandleError(err)
return nil, err
}
// Successfully grabbed params
params = paramList
default:
// Should be unreachable due to validation
return nil, fmt.Errorf("one of name or selector must be provided")
}
// Apply fail action for params not found case
if len(params) == 0 && paramRef.ParameterNotFoundAction != nil && *paramRef.ParameterNotFoundAction == v1.DenyAction {
return nil, errors.New("no params found for policy binding with `Deny` parameterNotFoundAction")
}
return params, nil
}
var _ webhookgeneric.VersionedAttributeAccessor = &versionedAttributeAccessor{}
type versionedAttributeAccessor struct {
versionedAttrs map[schema.GroupVersionKind]*admission.VersionedAttributes
attr admission.Attributes
objectInterfaces admission.ObjectInterfaces
}
func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionKind) (*admission.VersionedAttributes, error) {
if val, ok := v.versionedAttrs[gvk]; ok {
return val, nil
}
versionedAttr, err := admission.NewVersionedAttributes(v.attr, gvk, v.objectInterfaces)
if err != nil {
return nil, err
}
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}

View File

@ -0,0 +1,108 @@
/*
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 generic
import (
"fmt"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
)
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
type PolicyMatcher interface {
admission.InitializationValidator
// DefinitionMatches says whether this policy definition matches the provided admission
// resource request
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition PolicyAccessor) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error)
// BindingMatches says whether this policy definition matches the provided admission
// resource request
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding BindingAccessor) (bool, error)
// GetNamespace retrieves the Namespace resource by the given name. The name may be empty, in which case
// GetNamespace must return nil, nil
GetNamespace(name string) (*corev1.Namespace, error)
}
type matcher struct {
Matcher *matching.Matcher
}
func NewPolicyMatcher(m *matching.Matcher) PolicyMatcher {
return &matcher{
Matcher: m,
}
}
// ValidateInitialization checks if Matcher is initialized.
func (c *matcher) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition PolicyAccessor) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
constraints := definition.GetMatchConstraints()
if constraints == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("policy contained no match constraints, a required field")
}
criteria := matchCriteria{constraints: constraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding BindingAccessor) (bool, error) {
matchResources := binding.GetMatchResources()
if matchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: matchResources}
isMatch, _, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
func (c *matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return c.Matcher.GetNamespace(name)
}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *admissionregistrationv1.MatchResources
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() admissionregistrationv1.MatchResources {
return *m.constraints
}

View File

@ -0,0 +1,477 @@
/*
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 is the informer for the param CRD for this policy, or nil if
// there is no param or if there was a configuration error
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: %w", 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: %w", 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: %w. 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
}
var parsedParamKind *schema.GroupVersionKind
policyAccessor := s.newPolicyAccessor(policySpec)
if paramKind := policyAccessor.GetParamKind(); paramKind != nil {
groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion)
if err != nil {
errs = append(errs, fmt.Errorf("failed to parse paramKind APIVersion: %w", err))
continue
}
parsedParamKind = &schema.GroupVersionKind{
Group: groupVersion.Group,
Version: groupVersion.Version,
Kind: paramKind.Kind,
}
// TEMPORARY UNTIL WE HAVE SHARED PARAM INFORMERS
usedParams[*parsedParamKind] = struct{}{}
}
paramInformer, paramScope, configurationError := s.ensureParamsForPolicyLocked(parsedParamKind)
result = append(result, PolicyHook[P, B, E]{
Policy: policySpec,
Bindings: bindingSpecs,
Evaluator: s.compilePolicyLocked(policySpec),
ParamInformer: paramInformer,
ParamScope: paramScope,
ConfigurationError: configurationError,
})
// 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 := &paramInfo{
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
}

View File

@ -0,0 +1,639 @@
/*
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(),
fakeRestMapper,
)
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, _, err := p.inferGVK(object)
if err != nil {
return err
}
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, gvr, err := p.inferGVK(object)
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(gvr, object, objectMeta.GetNamespace()); err != nil {
if errors.IsAlreadyExists(err) {
return p.unstructuredTracker.Update(gvr, object, objectMeta.GetNamespace())
}
return err
}
return nil
} else if err := p.nativeTracker.Create(gvr, object, objectMeta.GetNamespace()); err != nil {
if errors.IsAlreadyExists(err) {
return p.nativeTracker.Update(gvr, 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
}
objectGVK, _, err := p.inferGVK(object)
if err != nil {
return err
}
if err := p.waitForDelete(
timeoutCtx,
objectGVK,
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, gvr, err := p.inferGVK(object)
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(gvr, objectMeta.GetNamespace(), objectMeta.GetName())
}
return p.nativeTracker.Delete(gvr, objectMeta.GetNamespace(), objectMeta.GetName())
}
}
func (p *PolicyTestContext[P, B, E]) Dispatch(
new, old runtime.Object,
operation admission.Operation,
) error {
if old == nil && new == nil {
return fmt.Errorf("both old and new objects cannot be nil")
}
nonNilObject := new
if nonNilObject == nil {
nonNilObject = old
}
gvk, gvr, err := p.inferGVK(nonNilObject)
if err != nil {
return err
}
nonNilMeta, err := meta.Accessor(nonNilObject)
if err != nil {
return err
}
return p.Plugin.Dispatch(
p,
admission.NewAttributesRecord(
new,
old,
gvk,
nonNilMeta.GetName(),
nonNilMeta.GetNamespace(),
gvr,
"",
operation,
nil,
false,
nil,
), admission.NewObjectInterfacesFromScheme(p.scheme))
}
func (p *PolicyTestContext[P, B, E]) inferGVK(object runtime.Object) (schema.GroupVersionKind, schema.GroupVersionResource, error) {
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 schema.GroupVersionKind{}, schema.GroupVersionResource{}, err
} else if len(knownKinds) == 0 {
return schema.GroupVersionKind{}, schema.GroupVersionResource{}, 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]
}
// 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 schema.GroupVersionKind{}, schema.GroupVersionResource{}, err
}
return objectGVK, mapping.Resource, nil
}
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
}

View File

@ -0,0 +1,293 @@
/*
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 generic
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/cache/synctrack"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
var _ Controller[runtime.Object] = &controller[runtime.Object]{}
type controller[T runtime.Object] struct {
informer Informer[T]
queue workqueue.RateLimitingInterface
// Returns an error if there was a transient error during reconciliation
// and the object should be tried again later.
reconciler func(namespace, name string, newObj T) error
options ControllerOptions
// must hold a func() bool or nil
notificationsDelivered atomic.Value
hasProcessed synctrack.AsyncTracker[string]
}
type ControllerOptions struct {
Name string
Workers uint
}
func (c *controller[T]) Informer() Informer[T] {
return c.informer
}
func NewController[T runtime.Object](
informer Informer[T],
reconciler func(namepace, name string, newObj T) error,
options ControllerOptions,
) Controller[T] {
if options.Workers == 0 {
options.Workers = 2
}
if len(options.Name) == 0 {
options.Name = fmt.Sprintf("%T-controller", *new(T))
}
c := &controller[T]{
options: options,
informer: informer,
reconciler: reconciler,
queue: nil,
}
c.hasProcessed.UpstreamHasSynced = func() bool {
f := c.notificationsDelivered.Load()
if f == nil {
return false
}
return f.(func() bool)()
}
return c
}
// Runs the controller and returns an error explaining why running was stopped.
// Reconciliation ends as soon as the context completes. If there are events
// waiting to be processed at that itme, they will be dropped.
func (c *controller[T]) Run(ctx context.Context) error {
klog.Infof("starting %s", c.options.Name)
defer klog.Infof("stopping %s", c.options.Name)
c.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), c.options.Name)
// Forcefully shutdown workqueue. Drop any enqueued items.
// Important to do this in a `defer` at the start of `Run`.
// Otherwise, if there are any early returns without calling this, we
// would never shut down the workqueue
defer c.queue.ShutDown()
enqueue := func(obj interface{}, isInInitialList bool) {
var key string
var err error
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
if isInInitialList {
c.hasProcessed.Start(key)
}
c.queue.Add(key)
}
registration, err := c.informer.AddEventHandler(cache.ResourceEventHandlerDetailedFuncs{
AddFunc: enqueue,
UpdateFunc: func(oldObj, newObj interface{}) {
oldMeta, err1 := meta.Accessor(oldObj)
newMeta, err2 := meta.Accessor(newObj)
if err1 != nil || err2 != nil {
if err1 != nil {
utilruntime.HandleError(err1)
}
if err2 != nil {
utilruntime.HandleError(err2)
}
return
} else if oldMeta.GetResourceVersion() == newMeta.GetResourceVersion() {
if len(oldMeta.GetResourceVersion()) == 0 {
klog.Warningf("%v throwing out update with empty RV. this is likely to happen if a test did not supply a resource version on an updated object", c.options.Name)
}
return
}
enqueue(newObj, false)
},
DeleteFunc: func(obj interface{}) {
// Enqueue
enqueue(obj, false)
},
})
// Error might be raised if informer was started and stopped already
if err != nil {
return err
}
c.notificationsDelivered.Store(registration.HasSynced)
// Make sure event handler is removed from informer in case return early from
// an error
defer func() {
c.notificationsDelivered.Store(func() bool { return false })
// Remove event handler and Handle Error here. Error should only be raised
// for improper usage of event handler API.
if err := c.informer.RemoveEventHandler(registration); err != nil {
utilruntime.HandleError(err)
}
}()
// Wait for initial cache list to complete before beginning to reconcile
// objects.
if !cache.WaitForNamedCacheSync(c.options.Name, ctx.Done(), c.informer.HasSynced) {
// ctx cancelled during cache sync. return early
err := ctx.Err()
if err == nil {
// if context wasnt cancelled then the sync failed for another reason
err = errors.New("cache sync failed")
}
return err
}
waitGroup := sync.WaitGroup{}
for i := uint(0); i < c.options.Workers; i++ {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
wait.Until(c.runWorker, time.Second, ctx.Done())
}()
}
klog.Infof("Started %v workers for %v", c.options.Workers, c.options.Name)
// Wait for context cancel.
<-ctx.Done()
// Forcefully shutdown workqueue. Drop any enqueued items.
c.queue.ShutDown()
// Workqueue shutdown signals for workers to stop. Wait for all workers to
// clean up
waitGroup.Wait()
// Only way for workers to ever stop is for caller to cancel the context
return ctx.Err()
}
func (c *controller[T]) HasSynced() bool {
return c.hasProcessed.HasSynced()
}
func (c *controller[T]) runWorker() {
for {
key, shutdown := c.queue.Get()
if shutdown {
return
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
// We call Done here so the workqueue knows we have finished
// processing this item. We also must remember to call Forget if we
// do not want this work item being re-queued. For example, we do
// not call Forget if a transient error occurs, instead the item is
// put back on the workqueue and attempted again after a back-off
// period.
defer c.queue.Done(obj)
var key string
var ok bool
// We expect strings to come off the workqueue. These are of the
// form namespace/name. We do this as the delayed nature of the
// workqueue means the items in the informer cache may actually be
// more up to date that when the item was initially put onto the
// workqueue.
if key, ok = obj.(string); !ok {
// How did an incorrectly formatted key get in the workqueue?
// Done is sufficient. (Forget resets rate limiter for the key,
// but the key is invalid so there is no point in doing that)
return fmt.Errorf("expected string in workqueue but got %#v", obj)
}
defer c.hasProcessed.Finished(key)
if err := c.reconcile(key); err != nil {
// Put the item back on the workqueue to handle any transient errors.
c.queue.AddRateLimited(key)
return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error())
}
// Finally, if no error occurs we Forget this item so it is allowed
// to be re-enqueued without a long rate limit
c.queue.Forget(obj)
klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
return nil
}(key)
if err != nil {
utilruntime.HandleError(err)
}
}
}
func (c *controller[T]) reconcile(key string) error {
var newObj T
var err error
var namespace string
var name string
var lister NamespacedLister[T]
// Convert the namespace/name string into a distinct namespace and name
namespace, name, err = cache.SplitMetaNamespaceKey(key)
if err != nil {
utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}
if len(namespace) > 0 {
lister = c.informer.Namespaced(namespace)
} else {
lister = c.informer
}
newObj, err = lister.Get(name)
if err != nil {
if !kerrors.IsNotFound(err) {
return err
}
// Deleted object. Inform reconciler with empty
}
return c.reconciler(namespace, name, newObj)
}

View File

@ -0,0 +1,29 @@
/*
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 generic contains a typed wrapper over cache SharedIndexInformer
// and Lister (maybe eventually should have a home there?)
//
// This interface is being experimented with as an easier way to write controllers
// with a bit less boilerplate.
//
// Informer/Lister classes are thin wrappers providing a type-safe interface
// over regular interface{}-based Informers/Listers
//
// Controller[T] provides a reusable way to reconcile objects out of an informer
// using the tried and true controller design pattern found all over k8s
// codebase based upon syncFunc/reconcile
package generic

View File

@ -0,0 +1,40 @@
/*
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 generic
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
var _ Informer[runtime.Object] = informer[runtime.Object]{}
type informer[T runtime.Object] struct {
cache.SharedIndexInformer
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] {
return informer[T]{
SharedIndexInformer: informe,
lister: NewLister[T](informe.GetIndexer()),
}
}

View File

@ -0,0 +1,62 @@
/*
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 generic
import (
"context"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
type Controller[T runtime.Object] interface {
// Meant to be run inside a goroutine
// Waits for and reacts to changes in whatever type the controller
// is concerned with.
//
// Returns an error always non-nil explaining why the worker stopped
Run(ctx context.Context) error
// Retrieves the informer used to back this controller
Informer() Informer[T]
// Returns true if the informer cache has synced, and all the objects from
// the initial list have been reconciled at least once.
HasSynced() bool
}
type NamespacedLister[T any] interface {
// List lists all ValidationRuleSets in the indexer for a given namespace.
// Objects returned here must be treated as read-only.
List(selector labels.Selector) (ret []T, err error)
// Get retrieves the ValidationRuleSet from the indexer for a given namespace and name.
// Objects returned here must be treated as read-only.
Get(name string) (T, error)
}
type Informer[T any] interface {
cache.SharedIndexInformer
Lister[T]
}
// Lister[T] helps list Ts.
// All objects returned here must be treated as read-only.
type Lister[T any] interface {
NamespacedLister[T]
Namespaced(namespace string) NamespacedLister[T]
}

View File

@ -0,0 +1,100 @@
/*
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 generic
import (
"fmt"
"net/http"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
)
var _ Lister[runtime.Object] = lister[runtime.Object]{}
type namespacedLister[T runtime.Object] struct {
indexer cache.Indexer
namespace string
}
func (w namespacedLister[T]) List(selector labels.Selector) (ret []T, err error) {
err = cache.ListAllByNamespace(w.indexer, w.namespace, selector, func(m interface{}) {
ret = append(ret, m.(T))
})
return ret, err
}
func (w namespacedLister[T]) Get(name string) (T, error) {
var result T
obj, exists, err := w.indexer.GetByKey(w.namespace + "/" + name)
if err != nil {
return result, err
}
if !exists {
return result, &kerrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotFound,
Reason: metav1.StatusReasonNotFound,
Message: fmt.Sprintf("%s not found", name),
}}
}
result = obj.(T)
return result, nil
}
type lister[T runtime.Object] struct {
indexer cache.Indexer
}
func (w lister[T]) List(selector labels.Selector) (ret []T, err error) {
err = cache.ListAll(w.indexer, selector, func(m interface{}) {
ret = append(ret, m.(T))
})
return ret, err
}
func (w lister[T]) Get(name string) (T, error) {
var result T
obj, exists, err := w.indexer.GetByKey(name)
if err != nil {
return result, err
}
if !exists {
// kerrors.StatusNotFound requires a GroupResource we cannot provide
return result, &kerrors.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusNotFound,
Reason: metav1.StatusReasonNotFound,
Message: fmt.Sprintf("%s not found", name),
}}
}
result = obj.(T)
return result, nil
}
func (w lister[T]) Namespaced(namespace string) NamespacedLister[T] {
return namespacedLister[T]{namespace: namespace, indexer: w.indexer}
}
func NewLister[T runtime.Object](indexer cache.Indexer) lister[T] {
return lister[T]{indexer: indexer}
}

View File

@ -0,0 +1,200 @@
/*
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 matching
import (
"fmt"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
)
type MatchCriteria interface {
namespace.NamespaceSelectorProvider
object.ObjectSelectorProvider
GetMatchResources() v1.MatchResources
}
// Matcher decides if a request matches against matchCriteria
type Matcher struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
}
func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return m.namespaceMatcher.GetNamespace(name)
}
// NewMatcher initialize the matcher with dependencies requires
func NewMatcher(
namespaceLister listersv1.NamespaceLister,
client kubernetes.Interface,
) *Matcher {
return &Matcher{
namespaceMatcher: &namespace.Matcher{
NamespaceLister: namespaceLister,
Client: client,
},
objectMatcher: &object.Matcher{},
}
}
// ValidateInitialization verify if the matcher is ready before use
func (m *Matcher) ValidateInitialization() error {
if err := m.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
}
return nil
}
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchNsErr == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchObjErr == nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
matchResources := criteria.GetMatchResources()
matchPolicy := matchResources.MatchPolicy
if isExcluded, _, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, err
}
var (
isMatch bool
matchResource schema.GroupVersionResource
matchKind schema.GroupVersionKind
matchErr error
)
if len(matchResources.ResourceRules) == 0 {
isMatch = true
matchKind = attr.GetKind()
matchResource = attr.GetResource()
} else {
isMatch, matchResource, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
}
if matchErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchErr
}
if !isMatch {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
// now that we know this applies to this request otherwise, if there were selector errors, return them
if matchNsErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchNsErr
}
if matchObjErr != nil {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, matchObjErr
}
return true, matchResource, matchKind, nil
}
func matchesResourceRules(namedRules []v1.NamedRuleWithOperations, matchPolicy *v1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionResource, schema.GroupVersionKind, error) {
matchKind := attr.GetKind()
matchResource := attr.GetResource()
for _, namedRule := range namedRules {
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
ruleMatcher := rules.Matcher{
Rule: rule,
Attr: attr,
}
if !ruleMatcher.Matches() {
continue
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, matchResource, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, matchResource, matchKind, nil
}
}
}
// if match policy is undefined or exact, don't perform fuzzy matching
// note that defaulting to fuzzy matching is set by the API
if matchPolicy == nil || *matchPolicy == v1.Exact {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
for _, namedRule := range namedRules {
for _, equivalent := range equivalents {
if equivalent == attr.GetResource() {
// we have already checked the original resource
continue
}
attrWithOverride.resource = equivalent
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
m := rules.Matcher{
Rule: rule,
Attr: attrWithOverride,
}
if !m.Matches() {
continue
}
matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
if matchKind.Empty() {
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, equivalent, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, equivalent, matchKind, nil
}
}
}
}
return false, schema.GroupVersionResource{}, schema.GroupVersionKind{}, nil
}
type attrWithResourceOverride struct {
admission.Attributes
resource schema.GroupVersionResource
}
func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }

View File

@ -0,0 +1,82 @@
/*
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/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewValidatingAdmissionPolicyAccessor(obj *v1.ValidatingAdmissionPolicy) generic.PolicyAccessor {
return &validatingAdmissionPolicyAccessor{
ValidatingAdmissionPolicy: obj,
}
}
func NewValidatingAdmissionPolicyBindingAccessor(obj *v1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor {
return &validatingAdmissionPolicyBindingAccessor{
ValidatingAdmissionPolicyBinding: obj,
}
}
type validatingAdmissionPolicyAccessor struct {
*v1.ValidatingAdmissionPolicy
}
func (v *validatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *validatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *validatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
return v.Spec.ParamKind
}
func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return v.Spec.MatchConstraints
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1.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,
}
}
func (v *validatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return v.Spec.MatchResources
}
func (v *validatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
return v.Spec.ParamRef
}

View File

@ -0,0 +1,133 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"context"
"encoding/json"
"sort"
"strings"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
type authzResult struct {
authorized authorizer.Decision
reason string
err error
}
type cachingAuthorizer struct {
authorizer authorizer.Authorizer
decisions map[string]authzResult
}
func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
return &cachingAuthorizer{
authorizer: in,
decisions: make(map[string]authzResult),
}
}
// The attribute accessors known to cache key construction. If this fails to compile, the cache
// implementation may need to be updated.
var _ authorizer.Attributes = (interface {
GetUser() user.Info
GetVerb() string
IsReadOnly() bool
GetNamespace() string
GetResource() string
GetSubresource() string
GetName() string
GetAPIGroup() string
GetAPIVersion() string
IsResourceRequest() bool
GetPath() string
})(nil)
// The user info accessors known to cache key construction. If this fails to compile, the cache
// implementation may need to be updated.
var _ user.Info = (interface {
GetName() string
GetUID() string
GetGroups() []string
GetExtra() map[string][]string
})(nil)
// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent
// check has already been performed, a cached result is returned. Not safe for concurrent use.
func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
serializableAttributes := authorizer.AttributesRecord{
Verb: a.GetVerb(),
Namespace: a.GetNamespace(),
APIGroup: a.GetAPIGroup(),
APIVersion: a.GetAPIVersion(),
Resource: a.GetResource(),
Subresource: a.GetSubresource(),
Name: a.GetName(),
ResourceRequest: a.IsResourceRequest(),
Path: a.GetPath(),
}
if u := a.GetUser(); u != nil {
di := &user.DefaultInfo{
Name: u.GetName(),
UID: u.GetUID(),
}
// Differently-ordered groups or extras could cause otherwise-equivalent checks to
// have distinct cache keys.
if groups := u.GetGroups(); len(groups) > 0 {
di.Groups = make([]string, len(groups))
copy(di.Groups, groups)
sort.Strings(di.Groups)
}
if extra := u.GetExtra(); len(extra) > 0 {
di.Extra = make(map[string][]string, len(extra))
for k, vs := range extra {
vdupe := make([]string, len(vs))
copy(vdupe, vs)
sort.Strings(vdupe)
di.Extra[k] = vdupe
}
}
serializableAttributes.User = di
}
var b strings.Builder
if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil {
return authorizer.DecisionNoOpinion, "", err
}
key := b.String()
if cached, ok := ca.decisions[key]; ok {
return cached.authorized, cached.reason, cached.err
}
authorized, reason, err := ca.authorizer.Authorize(ctx, a)
ca.decisions[key] = authzResult{
authorized: authorized,
reason: reason,
err: err,
}
return authorized, reason, err
}

View File

@ -0,0 +1,414 @@
/*
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 validating
import (
"context"
"errors"
"fmt"
"strings"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/warning"
"k8s.io/klog/v2"
)
type dispatcher struct {
matcher generic.PolicyMatcher
authz authorizer.Authorizer
}
var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
func NewDispatcher(
authorizer authorizer.Authorizer,
matcher generic.PolicyMatcher,
) generic.Dispatcher[PolicyHook] {
return &dispatcher{
matcher: matcher,
authz: authorizer,
}
}
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
// that determined the decision
type policyDecisionWithMetadata struct {
PolicyDecision
Definition *admissionregistrationv1.ValidatingAdmissionPolicy
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
}
// Dispatch implements generic.Dispatcher.
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
var deniedDecisions []policyDecisionWithMetadata
addConfigError := func(err error, definition *admissionregistrationv1.ValidatingAdmissionPolicy, binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding) {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy admissionregistrationv1.FailurePolicyType
if definition.Spec.FailurePolicy == nil {
policy = admissionregistrationv1.Fail
} else {
policy = *definition.Spec.FailurePolicy
}
// apply FailurePolicy specified in ValidatingAdmissionPolicy, the default would be Fail
switch policy {
case admissionregistrationv1.Ignore:
// TODO: add metrics for ignored error here
return
case admissionregistrationv1.Fail:
var message string
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err).Error()
} else {
message = fmt.Errorf("failed to configure binding: %w", err).Error()
}
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: message,
},
Definition: definition,
Binding: binding,
})
default:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
},
Definition: definition,
Binding: binding,
})
}
}
authz := newCachingAuthorizer(c.authz)
for _, hook := range hooks {
// 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
// conversion so that it is only performed when we know a policy matches,
// saving the cost of converting non-matching requests.
var versionedAttr *admission.VersionedAttributes
definition := hook.Policy
matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, NewValidatingAdmissionPolicyAccessor(definition))
if err != nil {
// Configuration error.
addConfigError(err, definition, nil)
continue
}
if !matches {
// Policy definition does not match request
continue
} else if hook.ConfigurationError != nil {
// Configuration error.
addConfigError(hook.ConfigurationError, definition, nil)
continue
}
auditAnnotationCollector := newAuditAnnotationCollector()
for _, binding := range hook.Bindings {
// If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it
matches, err := c.matcher.BindingMatches(a, o, NewValidatingAdmissionPolicyBindingAccessor(binding))
if err != nil {
// Configuration error.
addConfigError(err, definition, binding)
continue
}
if !matches {
continue
}
params, err := generic.CollectParams(
hook.Policy.Spec.ParamKind,
hook.ParamInformer,
hook.ParamScope,
binding.Spec.ParamRef,
a.GetNamespace(),
)
if err != nil {
addConfigError(err, definition, binding)
continue
} else if versionedAttr == nil && len(params) > 0 {
// As optimization versionedAttr creation is deferred until
// first use. Since > 0 params, we will validate
va, err := admission.NewVersionedAttributes(a, matchKind, o)
if err != nil {
wrappedErr := fmt.Errorf("failed to convert object version: %w", err)
addConfigError(wrappedErr, definition, binding)
continue
}
versionedAttr = va
}
var validationResults []ValidateResult
var namespace *v1.Namespace
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = c.matcher.GetNamespace(namespaceName)
if err != nil {
return err
}
}
for _, param := range params {
var p runtime.Object = param
if p != nil && p.GetObjectKind().GroupVersionKind().Empty() {
// Make sure param has TypeMeta populated
// This is a simple hack to make sure typeMeta is
// available to CEL without making copies of objects, etc.
p = &wrappedParam{
TypeMeta: metav1.TypeMeta{
APIVersion: definition.Spec.ParamKind.APIVersion,
Kind: definition.Spec.ParamKind.Kind,
},
nested: param,
}
}
validationResults = append(validationResults,
hook.Evaluator.Validate(
ctx,
matchResource,
versionedAttr,
p,
namespace,
celconfig.RuntimeCELCostBudget,
authz,
),
)
}
for _, validationResult := range validationResults {
for i, decision := range validationResult.Decisions {
switch decision.Action {
case ActionAdmit:
if decision.Evaluation == EvalError {
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
case ActionDeny:
for _, action := range binding.Spec.ValidationActions {
switch action {
case admissionregistrationv1.Deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case admissionregistrationv1.Audit:
publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case admissionregistrationv1.Warn:
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
celmetrics.Metrics.ObserveWarn(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
}
default:
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.Action, binding.Name, definition.Name)
}
}
for _, auditAnnotation := range validationResult.AuditAnnotations {
switch auditAnnotation.Action {
case AuditAnnotationActionPublish:
value := auditAnnotation.Value
if len(auditAnnotation.Value) > maxAuditAnnotationValueLength {
value = value[:maxAuditAnnotationValueLength]
}
auditAnnotationCollector.add(auditAnnotation.Key, value)
case AuditAnnotationActionError:
// When failurePolicy=fail, audit annotation errors result in deny
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Evaluation: EvalError,
Message: auditAnnotation.Error,
Elapsed: auditAnnotation.Elapsed,
},
})
celmetrics.Metrics.ObserveRejection(ctx, auditAnnotation.Elapsed, definition.Name, binding.Name, "active")
case AuditAnnotationActionExclude: // skip it
default:
return fmt.Errorf("unsupported AuditAnnotation Action: %s", auditAnnotation.Action)
}
}
}
}
auditAnnotationCollector.publish(definition.Name, a)
}
if len(deniedDecisions) > 0 {
// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
var message string
deniedDecision := deniedDecisions[0]
if deniedDecision.Binding != nil {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Binding.Name, deniedDecision.Message)
} else {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.Definition.Name, deniedDecision.Message)
}
err := admission.NewForbidden(a, errors.New(message)).(*k8serrors.StatusError)
reason := deniedDecision.Reason
if len(reason) == 0 {
reason = metav1.StatusReasonInvalid
}
err.ErrStatus.Reason = reason
err.ErrStatus.Code = reasonToCode(reason)
err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message})
return err
}
return nil
}
func publishValidationFailureAnnotation(binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
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
valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{
ExpressionIndex: expressionIndex,
Message: decision.Message,
ValidationActions: binding.Spec.ValidationActions,
Binding: binding.Name,
Policy: binding.Spec.PolicyName,
}})
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)
}
value := string(valueJSON)
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)
}
}
const maxAuditAnnotationValueLength = 10 * 1024
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
// annotation value.
type ValidationFailureValue struct {
Message string `json:"message"`
Policy string `json:"policy"`
Binding string `json:"binding"`
ExpressionIndex int `json:"expressionIndex"`
ValidationActions []admissionregistrationv1.ValidationAction `json:"validationActions"`
}
type auditAnnotationCollector struct {
annotations map[string][]string
}
func newAuditAnnotationCollector() auditAnnotationCollector {
return auditAnnotationCollector{annotations: map[string][]string{}}
}
func (a auditAnnotationCollector) add(key, value string) {
// If multiple bindings produces the exact same key and value for an audit annotation,
// ignore the duplicates.
for _, v := range a.annotations[key] {
if v == value {
return
}
}
a.annotations[key] = append(a.annotations[key], value)
}
func (a auditAnnotationCollector) publish(policyName string, attributes admission.Attributes) {
for key, bindingAnnotations := range a.annotations {
var value string
if len(bindingAnnotations) == 1 {
value = bindingAnnotations[0]
} else {
// Multiple distinct values can exist when binding params are used in the valueExpression of an auditAnnotation.
// When this happens, the values are concatenated into a comma-separated list.
value = strings.Join(bindingAnnotations, ", ")
}
if err := attributes.AddAnnotation(policyName+"/"+key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s: %v", key, value, policyName, err)
}
}
}
// A workaround to fact that native types do not have TypeMeta populated, which
// is needed for CEL expressions to be able to access the value.
type wrappedParam struct {
metav1.TypeMeta
nested runtime.Object
}
func (w *wrappedParam) MarshalJSON() ([]byte, error) {
return nil, errors.New("MarshalJSON unimplemented for wrappedParam")
}
func (w *wrappedParam) UnmarshalJSON(data []byte) error {
return errors.New("UnmarshalJSON unimplemented for wrappedParam")
}
func (w *wrappedParam) ToUnstructured() interface{} {
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(w.nested)
if err != nil {
return nil
}
metaRes, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&w.TypeMeta)
if err != nil {
return nil
}
for k, v := range metaRes {
res[k] = v
}
return res
}
func (w *wrappedParam) DeepCopyObject() runtime.Object {
return &wrappedParam{
TypeMeta: w.TypeMeta,
nested: w.nested.DeepCopyObject(),
}
}
func (w *wrappedParam) GetObjectKind() schema.ObjectKind {
return w
}

View File

@ -0,0 +1,31 @@
/*
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 validating
import (
"context"
"k8s.io/apiserver/pkg/admission"
)
type CELPolicyEvaluator interface {
admission.InitializationValidator
Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error
HasSynced() bool
Run(stopCh <-chan struct{})
}

View File

@ -0,0 +1,95 @@
/*
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 validating
import (
"context"
celgo "github.com/google/cel-go/cel"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
var _ cel.ExpressionAccessor = &ValidationCondition{}
// ValidationCondition contains the inputs needed to compile, evaluate and validate a cel expression
type ValidationCondition struct {
Expression string
Message string
Reason *metav1.StatusReason
}
func (v *ValidationCondition) GetExpression() string {
return v.Expression
}
func (v *ValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
// AuditAnnotationCondition contains the inputs needed to compile, evaluate and publish a cel audit annotation
type AuditAnnotationCondition struct {
Key string
ValueExpression string
}
func (v *AuditAnnotationCondition) GetExpression() string {
return v.ValueExpression
}
func (v *AuditAnnotationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType, celgo.NullType}
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
// ValidateResult defines the result of a Validator.Validate operation.
type ValidateResult struct {
// Decisions specifies the outcome of the validation as well as the details about the decision.
Decisions []PolicyDecision
// AuditAnnotations specifies the audit annotations that should be recorded for the validation.
AuditAnnotations []PolicyAuditAnnotation
}
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
}

View File

@ -0,0 +1,36 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
celgo "github.com/google/cel-go/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
var _ cel.ExpressionAccessor = (*MessageExpressionCondition)(nil)
type MessageExpressionCondition struct {
MessageExpression string
}
func (m *MessageExpressionCondition) GetExpression() string {
return m.MessageExpression
}
func (m *MessageExpressionCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType}
}

View File

@ -0,0 +1,123 @@
/*
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 cel
import (
"context"
"time"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
const (
metricsNamespace = "apiserver"
metricsSubsystem = "validating_admission_policy"
)
var (
// Metrics provides access to validation admission metrics.
Metrics = newValidationAdmissionMetrics()
)
// ValidatingAdmissionPolicyMetrics aggregates Prometheus metrics related to validation admission control.
type ValidatingAdmissionPolicyMetrics struct {
policyCheck *metrics.CounterVec
policyDefinition *metrics.CounterVec
policyLatency *metrics.HistogramVec
}
func newValidationAdmissionMetrics() *ValidatingAdmissionPolicyMetrics {
check := metrics.NewCounterVec(
&metrics.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubsystem,
Name: "check_total",
Help: "Validation admission policy check total, labeled by policy and further identified by binding, enforcement action taken, and state.",
StabilityLevel: metrics.ALPHA,
},
[]string{"policy", "policy_binding", "enforcement_action", "state"},
)
definition := metrics.NewCounterVec(&metrics.CounterOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubsystem,
Name: "definition_total",
Help: "Validation admission policy count total, labeled by state and enforcement action.",
StabilityLevel: metrics.ALPHA,
},
[]string{"state", "enforcement_action"},
)
latency := metrics.NewHistogramVec(&metrics.HistogramOpts{
Namespace: metricsNamespace,
Subsystem: metricsSubsystem,
Name: "check_duration_seconds",
Help: "Validation admission latency for individual validation expressions in seconds, labeled by policy and further including binding, state and enforcement action taken.",
// the bucket distribution here is based oo the benchmark suite at
// github.com/DangerOnTheRanger/cel-benchmark performed on 16-core Intel Xeon
// the lowest bucket was based around the 180ns/op figure for BenchmarkAccess,
// plus some additional leeway to account for the apiserver doing other things
// the largest bucket was chosen based on the fact that benchmarks indicate the
// same Xeon running a CEL expression close to the estimated cost limit takes
// around 760ms, so that bucket should only ever have the slowest CEL expressions
// in it
Buckets: []float64{0.0000005, 0.001, 0.01, 0.1, 1.0},
StabilityLevel: metrics.ALPHA,
},
[]string{"policy", "policy_binding", "enforcement_action", "state"},
)
legacyregistry.MustRegister(check)
legacyregistry.MustRegister(definition)
legacyregistry.MustRegister(latency)
return &ValidatingAdmissionPolicyMetrics{policyCheck: check, policyDefinition: definition, policyLatency: latency}
}
// Reset resets all validation admission-related Prometheus metrics.
func (m *ValidatingAdmissionPolicyMetrics) Reset() {
m.policyCheck.Reset()
m.policyDefinition.Reset()
m.policyLatency.Reset()
}
// ObserveDefinition observes a policy definition.
func (m *ValidatingAdmissionPolicyMetrics) ObserveDefinition(ctx context.Context, state, enforcementAction string) {
m.policyDefinition.WithContext(ctx).WithLabelValues(state, enforcementAction).Inc()
}
// ObserveAdmissionWithError observes a policy validation error that was ignored due to failure policy.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAdmissionWithError(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "allow", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "allow", state).Observe(elapsed.Seconds())
}
// ObserveRejection observes a policy validation error that was at least one of the reasons for a deny.
func (m *ValidatingAdmissionPolicyMetrics) ObserveRejection(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Observe(elapsed.Seconds())
}
// ObserveAudit observes a policy validation audit annotation was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAudit(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Observe(elapsed.Seconds())
}
// ObserveWarn observes a policy validation warning was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveWarn(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Observe(elapsed.Seconds())
}

View File

@ -0,0 +1,188 @@
/*
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/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 = v1.ValidatingAdmissionPolicy
type PolicyBinding = v1.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{}
var _ initializer.WantsExcludedAdmissionResources = &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().V1().ValidatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1().ValidatingAdmissionPolicyBindings().Informer(),
NewValidatingAdmissionPolicyAccessor,
NewValidatingAdmissionPolicyBindingAccessor,
compilePolicy,
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, generic.NewPolicyMatcher(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 := 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(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)
return res
}
func convertv1Validations(inputValidations []v1.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 convertv1MessageExpressions(inputValidations []v1.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 convertv1AuditAnnotations(inputValidations []v1.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 []v1.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
}

View File

@ -0,0 +1,87 @@
/*
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 validating
import (
"net/http"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PolicyDecisionAction string
const (
ActionAdmit PolicyDecisionAction = "admit"
ActionDeny PolicyDecisionAction = "deny"
)
type PolicyDecisionEvaluation string
const (
EvalAdmit PolicyDecisionEvaluation = "admit"
EvalError PolicyDecisionEvaluation = "error"
EvalDeny PolicyDecisionEvaluation = "deny"
)
// PolicyDecision contains the action determined from a cel evaluation along with metadata such as message, reason and duration
type PolicyDecision struct {
Action PolicyDecisionAction
Evaluation PolicyDecisionEvaluation
Message string
Reason metav1.StatusReason
Elapsed time.Duration
}
type PolicyAuditAnnotationAction string
const (
// AuditAnnotationActionPublish indicates that the audit annotation should be
// published with the audit event.
AuditAnnotationActionPublish PolicyAuditAnnotationAction = "publish"
// AuditAnnotationActionError indicates that the valueExpression resulted
// in an error.
AuditAnnotationActionError PolicyAuditAnnotationAction = "error"
// AuditAnnotationActionExclude indicates that the audit annotation should be excluded
// because the valueExpression evaluated to null, or because FailurePolicy is Ignore
// and the expression failed with a parse error, type check error, or runtime error.
AuditAnnotationActionExclude PolicyAuditAnnotationAction = "exclude"
)
type PolicyAuditAnnotation struct {
Key string
Value string
Elapsed time.Duration
Action PolicyAuditAnnotationAction
Error string
}
func reasonToCode(r metav1.StatusReason) int32 {
switch r {
case metav1.StatusReasonForbidden:
return http.StatusForbidden
case metav1.StatusReasonUnauthorized:
return http.StatusUnauthorized
case metav1.StatusReasonRequestEntityTooLarge:
return http.StatusRequestEntityTooLarge
case metav1.StatusReasonInvalid:
return http.StatusUnprocessableEntity
default:
// It should not reach here since we only allow above reason to be set from API level
return http.StatusUnprocessableEntity
}
}

View File

@ -0,0 +1,486 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/google/cel-go/cel"
"k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/klog/v2"
)
const maxTypesToCheck = 10
type TypeChecker struct {
SchemaResolver resolver.SchemaResolver
RestMapper meta.RESTMapper
}
// TypeCheckingContext holds information about the policy being type-checked.
// The struct is opaque to the caller.
type TypeCheckingContext struct {
gvks []schema.GroupVersionKind
declTypes []*apiservercel.DeclType
paramGVK schema.GroupVersionKind
paramDeclType *apiservercel.DeclType
variables []v1.Variable
}
type typeOverwrite struct {
object *apiservercel.DeclType
params *apiservercel.DeclType
}
// TypeCheckingResult holds the issues found during type checking, any returned
// error, and the gvk that the type checking is performed against.
type TypeCheckingResult struct {
// GVK is the associated GVK
GVK schema.GroupVersionKind
// Issues contain machine-readable information about the typechecking result.
Issues error
// Err is the possible error that was encounter during type checking.
Err error
}
// TypeCheckingResults is a collection of TypeCheckingResult
type TypeCheckingResults []*TypeCheckingResult
func (rs TypeCheckingResults) String() string {
var messages []string
for _, r := range rs {
message := r.String()
if message != "" {
messages = append(messages, message)
}
}
return strings.Join(messages, "\n")
}
// String converts the result to human-readable form as a string.
func (r *TypeCheckingResult) String() string {
if r.Issues == nil && r.Err == nil {
return ""
}
if r.Err != nil {
return fmt.Sprintf("%v: type checking error: %v\n", r.GVK, r.Err)
}
return fmt.Sprintf("%v: %s\n", r.GVK, r.Issues)
}
// Check preforms the type check against the given policy, and format the result
// as []ExpressionWarning that is ready to be set in policy.Status
// The result is nil if type checking returns no warning.
// The policy object is NOT mutated. The caller should update Status accordingly
func (c *TypeChecker) Check(policy *v1.ValidatingAdmissionPolicy) []v1.ExpressionWarning {
ctx := c.CreateContext(policy)
// warnings to return, note that the capacity is optimistically set to zero
var warnings []v1.ExpressionWarning // intentionally not setting capacity
// check main validation expressions and their message expressions, located in spec.validations[*]
fieldRef := field.NewPath("spec", "validations")
for i, v := range policy.Spec.Validations {
results := c.CheckExpression(ctx, v.Expression)
if len(results) != 0 {
warnings = append(warnings, v1.ExpressionWarning{
FieldRef: fieldRef.Index(i).Child("expression").String(),
Warning: results.String(),
})
}
// Note that MessageExpression is optional
if v.MessageExpression == "" {
continue
}
results = c.CheckExpression(ctx, v.MessageExpression)
if len(results) != 0 {
warnings = append(warnings, v1.ExpressionWarning{
FieldRef: fieldRef.Index(i).Child("messageExpression").String(),
Warning: results.String(),
})
}
}
return warnings
}
// CreateContext resolves all types and their schemas from a policy definition and creates the context.
func (c *TypeChecker) CreateContext(policy *v1.ValidatingAdmissionPolicy) *TypeCheckingContext {
ctx := new(TypeCheckingContext)
allGvks := c.typesToCheck(policy)
gvks := make([]schema.GroupVersionKind, 0, len(allGvks))
declTypes := make([]*apiservercel.DeclType, 0, len(allGvks))
for _, gvk := range allGvks {
declType, err := c.declType(gvk)
if err != nil {
// type checking errors MUST NOT alter the behavior of the policy
// even if an error occurs.
if !errors.Is(err, resolver.ErrSchemaNotFound) {
// Anything except ErrSchemaNotFound is an internal error
klog.V(2).ErrorS(err, "internal error: schema resolution failure", "gvk", gvk)
}
// skip for not found or internal error
continue
}
gvks = append(gvks, gvk)
declTypes = append(declTypes, declType)
}
ctx.gvks = gvks
ctx.declTypes = declTypes
paramsGVK := c.paramsGVK(policy) // maybe empty, correctly handled
paramsDeclType, err := c.declType(paramsGVK)
if err != nil {
if !errors.Is(err, resolver.ErrSchemaNotFound) {
klog.V(2).ErrorS(err, "internal error: cannot resolve schema for params", "gvk", paramsGVK)
}
paramsDeclType = nil
}
ctx.paramGVK = paramsGVK
ctx.paramDeclType = paramsDeclType
ctx.variables = policy.Spec.Variables
return ctx
}
func (c *TypeChecker) compiler(ctx *TypeCheckingContext, typeOverwrite typeOverwrite) (*plugincel.CompositedCompiler, error) {
envSet, err := buildEnvSet(
/* hasParams */ ctx.paramDeclType != nil,
/* hasAuthorizer */ true,
typeOverwrite)
if err != nil {
return nil, err
}
env, err := plugincel.NewCompositionEnv(plugincel.VariablesTypeName, envSet)
if err != nil {
return nil, err
}
compiler := &plugincel.CompositedCompiler{
Compiler: &typeCheckingCompiler{typeOverwrite: typeOverwrite, compositionEnv: env},
CompositionEnv: env,
}
return compiler, nil
}
// CheckExpression type checks a single expression, given the context
func (c *TypeChecker) CheckExpression(ctx *TypeCheckingContext, expression string) TypeCheckingResults {
var results TypeCheckingResults
for i, gvk := range ctx.gvks {
declType := ctx.declTypes[i]
compiler, err := c.compiler(ctx, typeOverwrite{
object: declType,
params: ctx.paramDeclType,
})
if err != nil {
utilruntime.HandleError(err)
continue
}
options := plugincel.OptionalVariableDeclarations{
HasParams: ctx.paramDeclType != nil,
HasAuthorizer: true,
}
compiler.CompileAndStoreVariables(convertv1beta1Variables(ctx.variables), options, environment.StoredExpressions)
result := compiler.CompileCELExpression(celExpression(expression), options, environment.StoredExpressions)
if err := result.Error; err != nil {
typeCheckingResult := &TypeCheckingResult{GVK: gvk}
if err.Type == apiservercel.ErrorTypeInvalid {
typeCheckingResult.Issues = err
} else {
typeCheckingResult.Err = err
}
results = append(results, typeCheckingResult)
}
}
return results
}
type celExpression string
func (c celExpression) GetExpression() string {
return string(c)
}
func (c celExpression) ReturnTypes() []*cel.Type {
return []*cel.Type{cel.AnyType}
}
func generateUniqueTypeName(kind string) string {
return fmt.Sprintf("%s%d", kind, time.Now().Nanosecond())
}
func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclType, error) {
if gvk.Empty() {
return nil, nil
}
s, err := c.SchemaResolver.ResolveSchema(gvk)
if err != nil {
return nil, err
}
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil
}
func (c *TypeChecker) paramsGVK(policy *v1.ValidatingAdmissionPolicy) schema.GroupVersionKind {
if policy.Spec.ParamKind == nil {
return schema.GroupVersionKind{}
}
gv, err := schema.ParseGroupVersion(policy.Spec.ParamKind.APIVersion)
if err != nil {
return schema.GroupVersionKind{}
}
return gv.WithKind(policy.Spec.ParamKind.Kind)
}
// typesToCheck extracts a list of GVKs that needs type checking from the policy
// the result is sorted in the order of Group, Version, and Kind
func (c *TypeChecker) typesToCheck(p *v1.ValidatingAdmissionPolicy) []schema.GroupVersionKind {
gvks := sets.New[schema.GroupVersionKind]()
if p.Spec.MatchConstraints == nil || len(p.Spec.MatchConstraints.ResourceRules) == 0 {
return nil
}
restMapperRefreshAttempted := false // at most once per policy, refresh RESTMapper and retry resolution.
for _, rule := range p.Spec.MatchConstraints.ResourceRules {
groups := extractGroups(&rule.Rule)
if len(groups) == 0 {
continue
}
versions := extractVersions(&rule.Rule)
if len(versions) == 0 {
continue
}
resources := extractResources(&rule.Rule)
if len(resources) == 0 {
continue
}
// sort GVRs so that the loop below provides
// consistent results.
sort.Strings(groups)
sort.Strings(versions)
sort.Strings(resources)
count := 0
for _, group := range groups {
for _, version := range versions {
for _, resource := range resources {
gvr := schema.GroupVersionResource{
Group: group,
Version: version,
Resource: resource,
}
resolved, err := c.RestMapper.KindsFor(gvr)
if err != nil {
if restMapperRefreshAttempted {
// RESTMapper refresh happens at most once per policy
continue
}
c.tryRefreshRESTMapper()
restMapperRefreshAttempted = true
resolved, err = c.RestMapper.KindsFor(gvr)
if err != nil {
continue
}
}
for _, r := range resolved {
if !r.Empty() {
gvks.Insert(r)
count++
// early return if maximum number of types are already
// collected
if count == maxTypesToCheck {
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
}
}
}
}
}
}
if gvks.Len() == 0 {
return nil
}
return sortGVKList(gvks.UnsortedList())
}
func extractGroups(rule *v1.Rule) []string {
groups := make([]string, 0, len(rule.APIGroups))
for _, group := range rule.APIGroups {
// give up if wildcard
if strings.ContainsAny(group, "*") {
return nil
}
groups = append(groups, group)
}
return groups
}
func extractVersions(rule *v1.Rule) []string {
versions := make([]string, 0, len(rule.APIVersions))
for _, version := range rule.APIVersions {
if strings.ContainsAny(version, "*") {
return nil
}
versions = append(versions, version)
}
return versions
}
func extractResources(rule *v1.Rule) []string {
resources := make([]string, 0, len(rule.Resources))
for _, resource := range rule.Resources {
// skip wildcard and subresources
if strings.ContainsAny(resource, "*/") {
continue
}
resources = append(resources, resource)
}
return resources
}
// sortGVKList sorts the list by Group, Version, and Kind
// returns the list itself.
func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
sort.Slice(list, func(i, j int) bool {
if g := strings.Compare(list[i].Group, list[j].Group); g != 0 {
return g < 0
}
if v := strings.Compare(list[i].Version, list[j].Version); v != 0 {
return v < 0
}
return strings.Compare(list[i].Kind, list[j].Kind) < 0
})
return list
}
// tryRefreshRESTMapper refreshes the RESTMapper if it supports refreshing.
func (c *TypeChecker) tryRefreshRESTMapper() {
if r, ok := c.RestMapper.(meta.ResettableRESTMapper); ok {
r.Reset()
}
}
func buildEnvSet(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*environment.EnvSet, error) {
baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
requestType := plugincel.BuildRequestType()
namespaceType := plugincel.BuildNamespaceType()
var varOpts []cel.EnvOption
var declTypes []*apiservercel.DeclType
// namespace, hand-crafted type
declTypes = append(declTypes, namespaceType)
varOpts = append(varOpts, createVariableOpts(namespaceType, plugincel.NamespaceVarName)...)
// request, hand-crafted type
declTypes = append(declTypes, requestType)
varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...)
// object and oldObject, same type, type(s) resolved from constraints
declTypes = append(declTypes, types.object)
varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...)
// params, defined by ParamKind
if hasParams && types.params != nil {
declTypes = append(declTypes, types.params)
varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...)
}
// authorizer, implicitly available to all expressions of a policy
if hasAuthorizer {
// we only need its structure but not the variable itself
varOpts = append(varOpts, cel.Variable("authorizer", library.AuthorizerType))
}
return baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: varOpts,
DeclTypes: declTypes,
},
)
}
// createVariableOpts creates a slice of EnvOption
// that can be used for creating a CEL env containing variables of declType.
// declType can be nil, in which case the variables will be of DynType.
func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption {
opts := make([]cel.EnvOption, 0, len(variables))
t := cel.DynType
if declType != nil {
t = declType.CelType()
}
for _, v := range variables {
opts = append(opts, cel.Variable(v, t))
}
return opts
}
type typeCheckingCompiler struct {
compositionEnv *plugincel.CompositionEnv
typeOverwrite typeOverwrite
}
// CompileCELExpression compiles the given expression.
// The implementation is the same as that of staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go
// except that:
// - object, oldObject, and params are typed instead of Dyn
// - compiler does not enforce the output type
// - the compiler does not initialize the program
func (c *typeCheckingCompiler) CompileCELExpression(expressionAccessor plugincel.ExpressionAccessor, options plugincel.OptionalVariableDeclarations, mode environment.Type) plugincel.CompilationResult {
resultError := func(errorString string, errType apiservercel.ErrorType) plugincel.CompilationResult {
return plugincel.CompilationResult{
Error: &apiservercel.Error{
Type: errType,
Detail: errorString,
},
ExpressionAccessor: expressionAccessor,
}
}
env, err := c.compositionEnv.Env(mode)
if err != nil {
return resultError(fmt.Sprintf("fail to build env: %v", err), apiservercel.ErrorTypeInternal)
}
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return resultError(issues.String(), apiservercel.ErrorTypeInvalid)
}
// type checker does not require the program, however the type must still be set.
return plugincel.CompilationResult{
OutputType: ast.OutputType(),
}
}
var _ plugincel.Compiler = (*typeCheckingCompiler)(nil)

View File

@ -0,0 +1,250 @@
/*
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 validating
import (
"context"
"fmt"
"strings"
celtypes "github.com/google/cel-go/common/types"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/klog/v2"
)
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
failPolicy *v1.FailurePolicyType
}
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
auditAnnotationFilter: auditAnnotationFilter,
messageFilter: messageFilter,
failPolicy: failPolicy,
}
}
func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
if f == v1.Ignore {
return ActionAdmit
}
return ActionDeny
}
func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnotationAction {
if f == v1.Ignore {
return AuditAnnotationActionExclude
}
return AuditAnnotationActionError
}
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVersionResource, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
} else {
f = *v.failPolicy
}
if v.celMatcher != nil {
matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams, authz)
if matchResults.Error != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: matchResults.Error.Error(),
},
},
}
}
// if preconditions are not met, then do not return any validations
if !matchResults.Matches {
return ValidateResult{}
}
}
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: authz}
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes, metav1.GroupVersionResource(matchedResource), metav1.GroupVersionKind(versionedAttr.VersionedKind))
// Decide which fields are exposed
ns := cel.CreateNamespaceObject(namespace)
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, ns, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
decisions := make([]PolicyDecision, len(evalResults))
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
klog.Error("Invalid type conversion to ValidationCondition")
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = "Invalid type sent to validator, expected ValidationCondition"
continue
}
var messageResult *cel.EvaluationResult
var messageError *apiservercel.Error
if len(messageResults) > i {
messageResult = &messageResults[i]
}
messageError, _ = err.(*apiservercel.Error)
if evalResult.Error != nil {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = evalResult.Error.Error()
} else if messageError != nil &&
(messageError.Type == apiservercel.ErrorTypeInternal ||
(messageError.Type == apiservercel.ErrorTypeInvalid &&
strings.HasPrefix(messageError.Detail, "validation failed due to running out of cost budget"))) {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
if validation.Reason == nil {
decision.Reason = metav1.StatusReasonInvalid
} else {
decision.Reason = *validation.Reason
}
// decide the failure message
var message string
// attempt to set message with messageExpression result
if messageResult != nil && messageResult.Error == nil && messageResult.EvalResult != nil {
// also fallback if the eval result is non-string (including null) or
// whitespaces.
if message, ok = messageResult.EvalResult.Value().(string); ok {
message = strings.TrimSpace(message)
// deny excessively long message from EvalResult
if len(message) > celconfig.MaxEvaluatedMessageExpressionSizeBytes {
klog.V(2).InfoS("excessively long message denied", "message", message)
message = ""
}
// deny message that contains newlines
if strings.ContainsAny(message, "\n") {
klog.V(2).InfoS("multi-line message denied", "message", message)
message = ""
}
}
}
if messageResult != nil && messageResult.Error != nil {
// log any error with messageExpression
klog.V(2).ErrorS(messageResult.Error, "error while evaluating messageExpression")
}
// fallback to set message to the custom message
if message == "" && len(validation.Message) > 0 {
message = strings.TrimSpace(validation.Message)
}
// fallback to use the expression to compose a message
if message == "" {
message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
decision.Message = message
} else {
decision.Action = ActionAdmit
decision.Evaluation = EvalAdmit
}
}
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, admissionRequest, options, namespace, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
for i, evalResult := range auditAnnotationEvalResults {
if evalResult.ExpressionAccessor == nil {
continue
}
var auditAnnotationResult = &auditAnnotationResults[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {
klog.Error("Invalid type conversion to AuditAnnotationCondition")
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = fmt.Sprintf("Invalid type sent to validator, expected AuditAnnotationCondition but got %T", evalResult.ExpressionAccessor)
continue
}
auditAnnotationResult.Key = validation.Key
if evalResult.Error != nil {
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = evalResult.Error.Error()
} else {
switch evalResult.EvalResult.Type() {
case celtypes.StringType:
value := strings.TrimSpace(evalResult.EvalResult.Value().(string))
if len(value) == 0 {
auditAnnotationResult.Action = AuditAnnotationActionExclude
} else {
auditAnnotationResult.Action = AuditAnnotationActionPublish
auditAnnotationResult.Value = value
}
case celtypes.NullType:
auditAnnotationResult.Action = AuditAnnotationActionExclude
default:
auditAnnotationResult.Action = AuditAnnotationActionError
auditAnnotationResult.Error = fmt.Sprintf("valueExpression '%v' resulted in unsupported return type: %v. "+
"Return type must be either string or null.", validation.ValueExpression, evalResult.EvalResult.Type())
}
}
}
return ValidateResult{Decisions: decisions, AuditAnnotations: auditAnnotationResults}
}