Integrate cel admission with API.

Co-authored-by: Alexander Zielenski <zielenski@google.com>
Co-authored-by: Joe Betz <jpbetz@google.com>
This commit is contained in:
Cici Huang 2022-11-07 21:38:55 +00:00
parent d86cfa9854
commit e7d83a1fb7
21 changed files with 1696 additions and 1018 deletions

View File

@ -20,6 +20,7 @@ package options
// This should probably be part of some configuration fed into the build for a
// given binary target.
import (
validatingpolicy "k8s.io/apiserver/pkg/admission/plugin/cel"
// Admission policies
"k8s.io/kubernetes/plugin/pkg/admission/admit"
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
@ -97,6 +98,7 @@ var AllOrderedPlugins = []string{
// webhook, resourcequota, and deny plugins must go at the end
mutatingwebhook.PluginName, // MutatingAdmissionWebhook
validatingpolicy.PluginName, // ValidatingAdmissionPolicy
validatingwebhook.PluginName, // ValidatingAdmissionWebhook
resourcequota.PluginName, // ResourceQuota
deny.PluginName, // AlwaysDeny
@ -159,6 +161,7 @@ func DefaultOffAdmissionPlugins() sets.String {
certsubjectrestriction.PluginName, // CertificateSubjectRestriction
defaultingressclass.PluginName, // DefaultIngressClass
podsecurity.PluginName, // PodSecurity
validatingpolicy.PluginName, // ValidatingAdmissionPolicy, only active when feature gate CELValidatingAdmission is enabled
)
return sets.NewString(AllOrderedPlugins...).Difference(defaultOnPlugins)

View File

@ -24,7 +24,7 @@ import (
func TestAdmissionPluginOrder(t *testing.T) {
// Ensure the last four admission plugins listed are webhooks, quota, and deny
allplugins := strings.Join(AllOrderedPlugins, ",")
expectSuffix := ",MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
expectSuffix := ",MutatingAdmissionWebhook,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook,ResourceQuota,AlwaysDeny"
if !strings.HasSuffix(allplugins, expectSuffix) {
t.Fatalf("AllOrderedPlugins must end with ...%s", expectSuffix)
}

View File

@ -75,7 +75,7 @@ var (
initEnvErr error
)
// This func is duplicated in k8s.io/apiserver/pkg/admission/plugin/cel/internal/implementation.go
// This func is duplicated in k8s.io/apiserver/pkg/admission/plugin/cel/validator.go
// If any changes are made here, consider to make the same changes there as well.
func getBaseEnv() (*cel.Env, error) {
initEnvOnce.Do(func() {

View File

@ -21,9 +21,16 @@ import (
"errors"
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
"time"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
@ -38,7 +45,7 @@ import (
const (
// PluginName indicates the name of admission plug-in
PluginName = "CEL"
PluginName = "ValidatingAdmissionPolicy"
)
// Register registers a plugin
@ -54,28 +61,89 @@ func Register(plugins *admission.Plugins) {
type celAdmissionPlugin struct {
evaluator CELPolicyEvaluator
inspectedFeatureGates bool
enabled bool
// Injected Dependencies
informerFactory informers.SharedInformerFactory
client kubernetes.Interface
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
stopCh <-chan struct{}
}
var _ WantsCELPolicyEvaluator = &celAdmissionPlugin{}
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
var _ admission.InitializationValidator = &celAdmissionPlugin{}
var _ admission.ValidationInterface = &celAdmissionPlugin{}
func NewPlugin() (*celAdmissionPlugin, error) {
func NewPlugin() (admission.Interface, error) {
result := &celAdmissionPlugin{}
return result, nil
}
func (c *celAdmissionPlugin) SetCELPolicyEvaluator(evaluator CELPolicyEvaluator) {
c.evaluator = evaluator
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.informerFactory = f
}
// Once clientset and informer factory are provided, creates and starts the
// admission controller
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
c.client = client
}
func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
c.restMapper = mapper
}
func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) {
c.dynamicClient = client
}
func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
if featureGates.Enabled(features.CELValidatingAdmission) {
c.enabled = true
}
c.inspectedFeatureGates = true
}
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *celAdmissionPlugin) ValidateInitialization() error {
if c.evaluator != nil {
if !c.inspectedFeatureGates {
return fmt.Errorf("%s did not see feature gates", PluginName)
}
if !c.enabled {
return nil
}
if c.informerFactory == nil {
return errors.New("missing informer factory")
}
if c.client == nil {
return errors.New("missing kubernetes client")
}
if c.restMapper == nil {
return errors.New("missing rest mapper")
}
if c.dynamicClient == nil {
return errors.New("missing dynamic client")
}
if c.stopCh == nil {
return errors.New("missing stop channel")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient)
if err := c.evaluator.ValidateInitialization(); err != nil {
return err
}
return errors.New("CELPolicyEvaluator not injected")
go c.evaluator.Run(c.stopCh)
return nil
}
////////////////////////////////////////////////////////////////////////////////
@ -91,13 +159,32 @@ func (c *celAdmissionPlugin) Validate(
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.enabled {
return nil
}
deadlined, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// isPolicyResource determines if an admission.Attributes object is describing
// the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
if isPolicyResource(a) {
return
}
if !cache.WaitForNamedCacheSync("cel-admission-plugin", deadlined.Done(), c.evaluator.HasSynced) {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
return c.evaluator.Validate(ctx, a, o)
}
func isPolicyResource(attr admission.Attributes) bool {
gvk := attr.GetResource()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
return true
}
}
return false
}

View File

@ -201,6 +201,17 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
},
}
}
_, err = cel.AstToCheckedExpr(ast)
if err != nil {
// should be impossible since env.Compile returned no issues
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: "unexpected compilation error: " + err.Error(),
},
}
}
prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),

View File

@ -21,28 +21,35 @@ import (
"errors"
"fmt"
"sync"
"sync/atomic"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission/plugin/cel/matching"
"k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"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/plugin/cel/internal/generic"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
var _ CELPolicyEvaluator = &celAdmissionController{}
// celAdmissionController is the top-level controller for admission control using CEL
// it is responsible for watching policy definitions, bindings, and config param CRDs
type celAdmissionController struct {
// Context under which the controller runs
runningContext context.Context
policyDefinitionsController generic.Controller[PolicyDefinition]
policyBindingController generic.Controller[PolicyBinding]
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
// dynamicclient used to create informers to watch the param crd types
dynamicClient dynamic.Interface
@ -50,7 +57,7 @@ type celAdmissionController struct {
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
objectConverter ObjectConverter
validatorCompiler ValidatorCompiler
// Lock which protects:
// - definitionInfo
@ -61,21 +68,26 @@ type celAdmissionController struct {
mutex sync.RWMutex
// controller and metadata
paramsCRDControllers map[schema.GroupVersionKind]*paramInfo
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
// Index for each definition namespace/name, contains all binding
// namespace/names known to exist for that definition
definitionInfo map[string]*definitionInfo
definitionInfo map[namespacedName]*definitionInfo
// Index for each bindings namespace/name. Contains compiled templates
// for the binding depending on the policy/param combination.
bindingInfos map[string]*bindingInfo
bindingInfos map[namespacedName]*bindingInfo
// Map from namespace/name of a definition to a set of namespace/name
// of bindings which depend on it.
// All keys must have at least one dependent binding
// All binding names MUST exist as a key bindingInfos
definitionsToBindings map[string]sets.String
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
}
// namespaceName is used as a key in definitionInfo and bindingInfos
type namespacedName struct {
namespace, name string
}
type definitionInfo struct {
@ -86,16 +98,16 @@ type definitionInfo struct {
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue PolicyDefinition
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicy
}
type bindingInfo struct {
// Compiled CEL expression turned into an evaluator
evaluator EvaluatorFunc
// Compiled CEL expression turned into an validator
validator atomic.Pointer[Validator]
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue PolicyBinding
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicyBinding
}
type paramInfo struct {
@ -106,31 +118,33 @@ type paramInfo struct {
stop func()
// Policy Definitions which refer to this param CRD
dependentDefinitions sets.String
dependentDefinitions sets.Set[namespacedName]
}
func NewAdmissionController(
// Informers
policyDefinitionsInformer cache.SharedIndexInformer,
policyBindingInformer cache.SharedIndexInformer,
// Injected Dependencies
objectConverter ObjectConverter,
informerFactory informers.SharedInformerFactory,
client kubernetes.Interface,
restMapper meta.RESTMapper,
dynamicClient dynamic.Interface,
) CELPolicyEvaluator {
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
validatorCompiler := &CELValidatorCompiler{
Matcher: matcher,
}
c := &celAdmissionController{
definitionInfo: make(map[string]*definitionInfo),
bindingInfos: make(map[string]*bindingInfo),
paramsCRDControllers: make(map[schema.GroupVersionKind]*paramInfo),
definitionsToBindings: make(map[string]sets.String),
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
dynamicClient: dynamicClient,
objectConverter: objectConverter,
validatorCompiler: validatorCompiler,
restMapper: restMapper,
}
c.policyDefinitionsController = generic.NewController(
generic.NewInformer[PolicyDefinition](policyDefinitionsInformer),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
c.reconcilePolicyDefinition,
generic.ControllerOptions{
Workers: 1,
@ -138,7 +152,8 @@ func NewAdmissionController(
},
)
c.policyBindingController = generic.NewController(
generic.NewInformer[PolicyBinding](policyBindingInformer),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
c.reconcilePolicyBinding,
generic.ControllerOptions{
Workers: 1,
@ -187,30 +202,57 @@ func (c *celAdmissionController) Validate(
c.mutex.RLock()
defer c.mutex.RUnlock()
var allDecisions []PolicyDecisionWithMetadata = nil
var deniedDecisions []policyDecisionWithMetadata
addConfigError := func(err error, definition PolicyDefinition, binding PolicyBinding) {
wrappedError := fmt.Errorf("configuration error: %w", err)
switch p := definition.GetFailurePolicy(); p {
case Ignore:
klog.Info(wrappedError)
addConfigError := func(err error, definition *v1alpha1.ValidatingAdmissionPolicy, binding *v1alpha1.ValidatingAdmissionPolicyBinding) {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1alpha1.FailurePolicyType
if definition.Spec.FailurePolicy == nil {
policy = v1alpha1.Fail
} else {
policy = *definition.Spec.FailurePolicy
}
// apply FailurePolicy specified in ValidatingAdmissionPolicy, the default would be Fail
switch policy {
case v1alpha1.Ignore:
// TODO: add metrics for ignored error here
return
case Fail:
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Kind: Deny,
Message: wrappedError.Error(),
case v1alpha1.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{
kind: deny,
message: message,
},
Definition: definition,
Binding: binding,
definition: definition,
binding: binding,
})
default:
utilruntime.HandleError(fmt.Errorf("unrecognized failure policy: '%v'", p))
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
policyDecision: policyDecision{
kind: deny,
message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
},
definition: definition,
binding: binding,
})
}
}
for definitionNamespacedName, definitionInfo := range c.definitionInfo {
definition := definitionInfo.lastReconciledValue
if !definition.Matches(a) {
matches, matchKind, err := c.validatorCompiler.DefinitionMatches(a, o, definition)
if err != nil {
// Configuration error.
addConfigError(err, definition, nil)
continue
}
if !matches {
// Policy definition does not match request
continue
} else if definitionInfo.configurationError != nil {
@ -221,8 +263,6 @@ func (c *celAdmissionController) Validate(
dependentBindings := c.definitionsToBindings[definitionNamespacedName]
if len(dependentBindings) == 0 {
// Definition has no known bindings yet.
addConfigError(errors.New("no bindings found"), definition, nil)
continue
}
@ -231,40 +271,61 @@ func (c *celAdmissionController) Validate(
// be a bindingInfo for it
bindingInfo := c.bindingInfos[namespacedBindingName]
binding := bindingInfo.lastReconciledValue
if !binding.Matches(a) {
matches, err := c.validatorCompiler.BindingMatches(a, o, binding)
if err != nil {
// Configuration error.
addConfigError(err, definition, binding)
continue
}
if !matches {
continue
}
var param *unstructured.Unstructured
// If definition has no paramsource, always provide nil params to
// evaluator. If binding specifies a params to use they are ignored.
// Done this way so you can configure params before definition is ready.
if paramSource := definition.GetParamSource(); paramSource != nil {
paramsNamespace, paramsName := binding.GetTargetParams()
// If definition has paramKind, paramRef is required in binding.
// If definition has no paramKind, paramRef set in binding will be ignored.
paramKind := definition.Spec.ParamKind
paramRef := binding.Spec.ParamRef
if paramKind != nil && paramRef != nil {
// Find the params referred by the binding by looking its name up
// in our informer for its CRD
paramInfo, ok := c.paramsCRDControllers[*paramSource]
paramInfo, ok := c.paramsCRDControllers[*paramKind]
if !ok {
addConfigError(fmt.Errorf("paramSource kind `%v` not known",
paramSource.String()), definition, binding)
addConfigError(fmt.Errorf("paramKind kind `%v` not known",
paramKind.String()), definition, binding)
continue
}
if len(paramsNamespace) == 0 {
param, err = paramInfo.controller.Informer().Get(paramsName)
// If the param informer for this admission policy has not yet
// had time to perform an initial listing, don't attempt to use
// it.
//!TOOD(alexzielenski): add a wait for a very short amount of
// time for the cache to sync
if !paramInfo.controller.HasSynced() {
addConfigError(fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String()), definition, binding)
continue
}
if len(paramRef.Namespace) == 0 {
param, err = paramInfo.controller.Informer().Get(paramRef.Name)
} else {
param, err = paramInfo.controller.Informer().Namespaced(paramsNamespace).Get(paramsName)
param, err = paramInfo.controller.Informer().Namespaced(paramRef.Namespace).Get(paramRef.Name)
}
if err != nil {
// Apply failure policy
addConfigError(err, definition, binding)
if k8serrors.IsNotFound(err) {
// Param doesnt exist yet?
// Maybe just have to wait a bit.
if k8serrors.IsInvalid(err) {
// Param mis-configured
// require to set paramRef.namespace for namespaced resource and unset paramRef.namespace for cluster scoped resource
continue
} else if k8serrors.IsNotFound(err) {
// Param not yet available. User may need to wait a bit
// before being able to use it for validation.
continue
}
@ -274,47 +335,69 @@ func (c *celAdmissionController) Validate(
}
}
if bindingInfo.evaluator == nil {
validator := bindingInfo.validator.Load()
if validator == nil {
// Compile policy definition using binding
bindingInfo.evaluator, err = definition.Compile(c.objectConverter, c.restMapper)
if err != nil {
// compilation error. Apply failure policy
wrappedError := fmt.Errorf("failed to compile CEL expression: %w", err)
addConfigError(wrappedError, definition, binding)
continue
}
c.bindingInfos[namespacedBindingName] = bindingInfo
newValidator := c.validatorCompiler.Compile(definition)
validator = &newValidator
bindingInfo.validator.Store(validator)
}
decisions, err := (*validator).Validate(a, o, param, matchKind)
if err != nil {
// runtime error. Apply failure policy
wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err)
addConfigError(wrappedError, definition, binding)
continue
}
decisions := bindingInfo.evaluator(a, param)
for _, decision := range decisions {
switch decision.Kind {
case Admit:
// Do nothing
case Deny:
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
switch decision.kind {
case admit:
// TODO: add metrics for ignored error here
case deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
definition: definition,
binding: binding,
policyDecision: decision,
})
default:
// unrecognized decision. ignore
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.kind, binding.Name, definition.Name)
}
}
}
}
if len(allDecisions) > 0 {
return k8serrors.NewConflict(
a.GetResource().GroupResource(), a.GetName(),
&PolicyError{
Decisions: allDecisions,
})
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 (c *celAdmissionController) HasSynced() bool {
return c.policyBindingController.HasSynced() &&
c.policyDefinitionsController.HasSynced()
}
func (c *celAdmissionController) ValidateInitialization() error {
return c.validatorCompiler.ValidateInitialization()
}

View File

@ -21,6 +21,7 @@ import (
"fmt"
"time"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -30,28 +31,27 @@ import (
"k8s.io/client-go/tools/cache"
)
func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name string, definition PolicyDefinition) error {
func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
c.mutex.Lock()
defer c.mutex.Unlock()
// Namespace for policydefinition is empty. Leaving usage here for compatibility
// with future NamespacedPolicyDefinition
namespacedName := namespace + "/" + name
info, ok := c.definitionInfo[namespacedName]
// Namespace for policydefinition is empty.
nn := getNamespaceName(namespace, name)
info, ok := c.definitionInfo[nn]
if !ok {
info = &definitionInfo{}
c.definitionInfo[namespacedName] = info
c.definitionInfo[nn] = info
}
var paramSource *schema.GroupVersionKind
var paramSource *v1alpha1.ParamKind
if definition != nil {
paramSource = definition.GetParamSource()
paramSource = definition.Spec.ParamKind
}
// If param source has changed, remove definition as dependent of old params
// If there are no more dependents of old param, stop and clean up controller
if info.lastReconciledValue != nil && info.lastReconciledValue.GetParamSource() != nil {
oldParamSource := *info.lastReconciledValue.GetParamSource()
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
// If we are:
// - switching from having a param to not having a param (includes deletion)
@ -59,7 +59,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
// we remove dependency on the controller.
if paramSource == nil || *paramSource != oldParamSource {
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
oldParamInfo.dependentDefinitions.Delete(namespacedName)
oldParamInfo.dependentDefinitions.Delete(nn)
if len(oldParamInfo.dependentDefinitions) == 0 {
oldParamInfo.stop()
delete(c.paramsCRDControllers, oldParamSource)
@ -70,14 +70,14 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
// Reset all previously compiled evaluators in case something relevant in
// definition has changed.
for key := range c.definitionsToBindings[namespacedName] {
for key := range c.definitionsToBindings[nn] {
bindingInfo := c.bindingInfos[key]
bindingInfo.evaluator = nil
bindingInfo.validator.Store(nil)
c.bindingInfos[key] = bindingInfo
}
if definition == nil {
delete(c.definitionInfo, namespacedName)
delete(c.definitionInfo, nn)
return nil
}
@ -91,12 +91,28 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
}
// find GVR for params
paramsGVR, err := c.restMapper.RESTMapping(paramSource.GroupKind(), paramSource.Version)
// Parse param source into a GVK
paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
// Return nil, since this error cannot be resolved by waiting more time
return nil
}
paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
Group: paramSourceGV.Group,
Kind: paramSource.Kind,
}, paramSourceGV.Version)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
//
info.configurationError = fmt.Errorf("failed to find resource for param source: '%v'", paramSource.String())
info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
return info.configurationError
}
@ -126,7 +142,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
c.paramsCRDControllers[*paramSource] = &paramInfo{
controller: controller,
stop: instanceCancel,
dependentDefinitions: sets.NewString(namespacedName),
dependentDefinitions: sets.New(nn),
}
go informer.Informer().Run(instanceContext.Done())
@ -136,37 +152,37 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
return nil
}
func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string, binding PolicyBinding) error {
func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string, binding *v1alpha1.ValidatingAdmissionPolicyBinding) error {
c.mutex.Lock()
defer c.mutex.Unlock()
// Namespace for PolicyBinding is empty. In the future a namespaced binding
// may be added
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
namespacedName := namespace + "/" + name
info, ok := c.bindingInfos[namespacedName]
nn := getNamespaceName(namespace, name)
info, ok := c.bindingInfos[nn]
if !ok {
info = &bindingInfo{}
c.bindingInfos[namespacedName] = info
c.bindingInfos[nn] = info
}
oldNamespacedDefinitionName := ""
var oldNamespacedDefinitionName namespacedName
if info.lastReconciledValue != nil {
oldefinitionNamespace, oldefinitionName := info.lastReconciledValue.GetTargetDefinition()
oldNamespacedDefinitionName = oldefinitionNamespace + "/" + oldefinitionName
// All validating policies are cluster-scoped so have empty namespace
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
}
namespacedDefinitionName := ""
var namespacedDefinitionName namespacedName
if binding != nil {
newDefinitionNamespace, newDefinitionName := binding.GetTargetDefinition()
namespacedDefinitionName = newDefinitionNamespace + "/" + newDefinitionName
// All validating policies are cluster-scoped so have empty namespace
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
}
// Remove record of binding from old definition if the referred policy
// has changed
if oldNamespacedDefinitionName != namespacedDefinitionName {
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
dependentBindings.Delete(namespacedName)
dependentBindings.Delete(nn)
// if there are no more dependent bindings, remove knowledge of the
// definition altogether
@ -177,19 +193,19 @@ func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string,
}
if binding == nil {
delete(c.bindingInfos, namespacedName)
delete(c.bindingInfos, nn)
return nil
}
// Add record of binding to new definition
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
dependentBindings.Insert(namespacedName)
dependentBindings.Insert(nn)
} else {
c.definitionsToBindings[namespacedDefinitionName] = sets.NewString(namespacedName)
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
}
// Remove compiled template for old binding
info.evaluator = nil
info.validator.Store(nil)
info.lastReconciledValue = binding
return nil
}
@ -201,3 +217,10 @@ func (c *celAdmissionController) reconcileParams(namespace, name string, params
// checker errors to the status of the resources.
return nil
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,
name: name,
}
}

View File

@ -1,258 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"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"
"k8s.io/apiserver/pkg/admission"
)
////////////////////////////////////////////////////////////////////////////////
// Fake Policy Definitions
////////////////////////////////////////////////////////////////////////////////
type FakePolicyDefinition struct {
metav1.TypeMeta
metav1.ObjectMeta
// Function called when `Matches` is called
// If nil, a default function that always returns true is used
// Specified as a function pointer so that this type is still comparable
MatchFunc *func(admission.Attributes) bool `json:"-"`
// Func invoked for implementation of `Compile`
// Specified as a function pointer so that this type is still comparable
CompileFunc *func(converter ObjectConverter) (EvaluatorFunc, error) `json:"-"`
// GVK to return when ParamSource() is called
ParamSource *schema.GroupVersionKind `json:"paramSource"`
FailurePolicy FailurePolicy `json:"failurePolicy"`
}
var _ PolicyDefinition = &FakePolicyDefinition{}
func (f *FakePolicyDefinition) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyDefinition) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyDefinition",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyDefinition) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyDefinition) DeepCopyObject() runtime.Object {
copied := *f
f.ObjectMeta.DeepCopyInto(&copied.ObjectMeta)
return &copied
}
func (f *FakePolicyDefinition) GetName() string {
return f.ObjectMeta.Name
}
func (f *FakePolicyDefinition) GetNamespace() string {
return f.ObjectMeta.Namespace
}
func (f *FakePolicyDefinition) Matches(a admission.Attributes) bool {
if f.MatchFunc == nil || *f.MatchFunc == nil {
return true
}
return (*f.MatchFunc)(a)
}
func (f *FakePolicyDefinition) Compile(
converter ObjectConverter,
mapper meta.RESTMapper,
) (EvaluatorFunc, error) {
if f.CompileFunc == nil || *f.CompileFunc == nil {
panic("must provide a CompileFunc to policy definition")
}
return (*f.CompileFunc)(converter)
}
func (f *FakePolicyDefinition) GetParamSource() *schema.GroupVersionKind {
return f.ParamSource
}
func (f *FakePolicyDefinition) GetFailurePolicy() FailurePolicy {
return f.FailurePolicy
}
////////////////////////////////////////////////////////////////////////////////
// Fake Policy Binding
////////////////////////////////////////////////////////////////////////////////
type FakePolicyBinding struct {
metav1.TypeMeta
metav1.ObjectMeta
// Specified as a function pointer so that this type is still comparable
MatchFunc *func(admission.Attributes) bool `json:"-"`
Params string `json:"params"`
Policy string `json:"policy"`
}
var _ PolicyBinding = &FakePolicyBinding{}
func (f *FakePolicyBinding) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyBinding) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyBinding",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyBinding) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyBinding) DeepCopyObject() runtime.Object {
copied := *f
f.ObjectMeta.DeepCopyInto(&copied.ObjectMeta)
return &copied
}
func (f *FakePolicyBinding) Matches(a admission.Attributes) bool {
if f.MatchFunc == nil || *f.MatchFunc == nil {
return true
}
return (*f.MatchFunc)(a)
}
func (f *FakePolicyBinding) GetTargetDefinition() (namespace, name string) {
return f.Namespace, f.Policy
}
func (f *FakePolicyBinding) GetTargetParams() (namespace, name string) {
return f.Namespace, f.Params
}
/// List Types
type FakePolicyDefinitionList struct {
metav1.TypeMeta
metav1.ListMeta
Items []FakePolicyDefinition
}
func (f *FakePolicyDefinitionList) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyDefinitionList) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyDefinitionList",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyDefinitionList) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyDefinitionList) DeepCopyObject() runtime.Object {
copied := *f
f.ListMeta.DeepCopyInto(&copied.ListMeta)
copied.Items = make([]FakePolicyDefinition, len(f.Items))
copy(copied.Items, f.Items)
return &copied
}
type FakePolicyBindingList struct {
metav1.TypeMeta
metav1.ListMeta
Items []FakePolicyBinding
}
func (f *FakePolicyBindingList) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyBindingList) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyBindingList",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyBindingList) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyBindingList) DeepCopyObject() runtime.Object {
copied := *f
f.ListMeta.DeepCopyInto(&copied.ListMeta)
copied.Items = make([]FakePolicyBinding, len(f.Items))
copy(copied.Items, f.Items)
return &copied
}

View File

@ -18,38 +18,13 @@ package cel
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
}
// NewPluginInitializer creates a plugin initializer which dependency injects a
// singleton cel admission controller into the plugins which desire it
func NewPluginInitializer(validator CELPolicyEvaluator) *PluginInitializer {
return &PluginInitializer{validator: validator}
}
// WantsCELPolicyEvaluator gives the ability to have the shared
// CEL Admission Controller dependency injected at initialization-time.
type WantsCELPolicyEvaluator interface {
SetCELPolicyEvaluator(CELPolicyEvaluator)
}
// PluginInitializer is used for initialization of the webhook admission plugin.
type PluginInitializer struct {
validator CELPolicyEvaluator
}
var _ admission.PluginInitializer = &PluginInitializer{}
// Initialize checks the initialization interfaces implemented by each plugin
// and provide the appropriate initialization data
func (i *PluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsCELPolicyEvaluator); ok {
wants.SetCELPolicyEvaluator(i.validator)
}
Run(stopCh <-chan struct{})
}

View File

@ -17,92 +17,34 @@ limitations under the License.
package cel
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"github.com/google/cel-go/common/types/ref"
)
type FailurePolicy string
const (
Fail FailurePolicy = "Fail"
Ignore FailurePolicy = "Ignore"
)
// EvaluatorFunc represents the AND of one or more compiled CEL expression's
// evaluators `params` may be nil if definition does not specify a paramsource
type EvaluatorFunc func(a admission.Attributes, params *unstructured.Unstructured) []PolicyDecision
// ObjectConverter is Dependency Injected into the PolicyDefinition's `Compile`
// function to assist with converting types and values to/from CEL-typed values.
type ObjectConverter interface {
// DeclForResource looks up the openapi or JSONSchemaProps, structural schema, etc.
// and compiles it into something that can be used to turn objects into CEL
// values
DeclForResource(gvr schema.GroupVersionResource) (*cel.DeclType, error)
// ValueForObject takes a Kubernetes Object and uses the CEL DeclType
// to transform it into a CEL value.
// Object may be a typed native object or an unstructured object
ValueForObject(value runtime.Object, decl *cel.DeclType) (ref.Val, error)
// Validator defines the func used to validate the cel expressions
// matchKind provides the GroupVersionKind that the object should be
// validated by CEL expressions as.
type Validator interface {
Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error)
}
// PolicyDefinition is an interface for internal policy binding type.
// Implemented by mock/testing types, and to be implemented by the public API
// types once they have completed API review.
//
// The interface closely mirrors the format and functionality of the
// PolicyDefinition proposed in the KEP.
type PolicyDefinition interface {
runtime.Object
// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile`
// function to assist with converting types and values to/from CEL-typed values.
type ValidatorCompiler interface {
admission.InitializationValidator
// Matches says whether this policy definition matches the provided admission
// resource request
Matches(a admission.Attributes) bool
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
Compile(
// Definition is provided with a converter which may be used by the
// return evaluator function to convert objects into CEL-typed objects
objectConverter ObjectConverter,
// Injected RESTMapper to assist with compilation
mapper meta.RESTMapper,
) (EvaluatorFunc, error)
// GetParamSource returns the GVK for the CRD used as the source of
// parameters used in the evaluation of instances of this policy
// May return nil if there is no paramsource for this definition.
GetParamSource() *schema.GroupVersionKind
// GetFailurePolicy returns how an object should be treated during an
// admission when there is a configuration error preventing CEL evaluation
GetFailurePolicy() FailurePolicy
}
// PolicyBinding is an interface for internal policy binding type. Implemented
// by mock/testing types, and to be implemented by the public API types once
// they have completed API review.
//
// The interface closely mirrors the format and functionality of the
// PolicyBinding proposed in the KEP.
type PolicyBinding interface {
runtime.Object
// Matches says whether this policy binding matches the provided admission
// Matches says whether this policy definition matches the provided admission
// resource request
Matches(a admission.Attributes) bool
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
// GetTargetDefinition returns the Namespace/Name of Policy Definition used
// by this binding.
GetTargetDefinition() (namespace, name string)
// GetTargetParams returns the Namespace/Name of instance of TargetDefinition's
// ParamSource to be provided to the CEL expressions of the definition during
// evaluation.
// If TargetDefinition has nil ParamSource, this is ignored.
GetTargetParams() (namespace, name string)
// Compile is used for the cel expression compilation
Compile(
policy *v1alpha1.ValidatingAdmissionPolicy,
) Validator
}

View File

@ -52,7 +52,7 @@ type ControllerOptions struct {
Workers uint
}
func (c controller[T]) Informer() Informer[T] {
func (c *controller[T]) Informer() Informer[T] {
return c.informer
}
@ -73,7 +73,7 @@ func NewController[T runtime.Object](
options: options,
informer: informer,
reconciler: reconciler,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), options.Name),
queue: nil,
}
}
@ -84,10 +84,18 @@ 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{}) {
var key string
var err error
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
@ -185,7 +193,7 @@ func (c *controller[T]) HasSynced() bool {
func (c *controller[T]) runWorker() {
for {
obj, shutdown := c.queue.Get()
key, shutdown := c.queue.Get()
if shutdown {
return
}
@ -221,9 +229,9 @@ func (c *controller[T]) runWorker() {
// 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.Infof("Successfully synced '%s'", key)
klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
return nil
}(obj)
}(key)
if err != nil {
utilruntime.HandleError(err)

View File

@ -17,37 +17,43 @@ limitations under the License.
package cel
import (
"encoding/json"
"fmt"
"net/http"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PolicyDecisionKind string
type policyDecisionKind string
const (
Admit PolicyDecisionKind = "Admit"
Deny PolicyDecisionKind = "Deny"
admit policyDecisionKind = "admit"
deny policyDecisionKind = "deny"
)
type PolicyDecision struct {
Kind PolicyDecisionKind `json:"kind"`
Message any `json:"message"`
type policyDecision struct {
kind policyDecisionKind
message string
reason metav1.StatusReason
}
type PolicyDecisionWithMetadata struct {
PolicyDecision `json:"decision"`
Definition PolicyDefinition `json:"definition"`
Binding PolicyBinding `json:"binding"`
type policyDecisionWithMetadata struct {
policyDecision
definition *v1alpha1.ValidatingAdmissionPolicy
binding *v1alpha1.ValidatingAdmissionPolicyBinding
}
type PolicyError struct {
Decisions []PolicyDecisionWithMetadata
}
func (p *PolicyError) Error() string {
// Just format the error as JSON
jsonText, err := json.Marshal(p.Decisions)
if err != nil {
return fmt.Sprintf("error formatting PolicyError: %s", err.Error())
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
}
return string(jsonText)
}

View File

@ -0,0 +1,310 @@
/*
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 (
"fmt"
"reflect"
"strings"
celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/api/admissionregistration/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel/matching"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
var _ ValidatorCompiler = &CELValidatorCompiler{}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *v1alpha1.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() v1alpha1.MatchResources {
return *m.constraints
}
// CELValidatorCompiler implement the interface ValidatorCompiler.
type CELValidatorCompiler struct {
Matcher *matching.Matcher
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *CELValidatorCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *CELValidatorCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) {
if binding.Spec.MatchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
// ValidateInitialization checks if Matcher is initialized.
func (c *CELValidatorCompiler) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
type validationActivation struct {
object, oldObject, params, request interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true
case RequestVarName:
return a.request, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *validationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expression defined in ValidatingAdmissionPolicy
func (c *CELValidatorCompiler) Compile(p *v1alpha1.ValidatingAdmissionPolicy) Validator {
if len(p.Spec.Validations) == 0 {
return nil
}
hasParam := false
if p.Spec.ParamKind != nil {
hasParam = true
}
compilationResults := make([]CompilationResult, len(p.Spec.Validations))
for i, validation := range p.Spec.Validations {
compilationResults[i] = CompileValidatingPolicyExpression(validation.Expression, hasParam)
}
return &CELValidator{policy: p, compilationResults: compilationResults}
}
// CELValidator implements the Validator interface
type CELValidator struct {
policy *v1alpha1.ValidatingAdmissionPolicy
compilationResults []CompilationResult
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
func policyDecisionKindForError(f v1alpha1.FailurePolicyType) policyDecisionKind {
if f == v1alpha1.Ignore {
return admit
}
return deny
}
// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error.
// An error will be returned if failed to convert the object/oldObject/params/request to unstructured.
// Each PolicyDecision will have a decision and a message.
// policyDecision.message will be empty if the decision is allowed and no error met.
func (v *CELValidator) Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
decisions := make([]policyDecision, len(v.compilationResults))
var err error
versionedAttr, err := generic.NewVersionedAttributes(a, matchKind, o)
if err != nil {
return nil, err
}
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, err
}
paramsVal, err := objectToResolveVal(versionedParams)
if err != nil {
return nil, err
}
request := createAdmissionRequest(versionedAttr.Attributes)
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, err
}
va := &validationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
}
var f v1alpha1.FailurePolicyType
if v.policy.Spec.FailurePolicy == nil {
f = v1alpha1.Fail
} else {
f = *v.policy.Spec.FailurePolicy
}
for i, compilationResult := range v.compilationResults {
validation := v.policy.Spec.Validations[i]
var policyDecision = &decisions[i]
if compilationResult.Error != nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = fmt.Sprintf("compilation error: %v", compilationResult.Error)
continue
}
if compilationResult.Program == nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = "unexpected internal error compiling expression"
continue
}
evalResult, _, err := compilationResult.Program.Eval(va)
if err != nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = fmt.Sprintf("expression '%v' resulted in error: %v", v.policy.Spec.Validations[i].Expression, err)
} else if evalResult != celtypes.True {
policyDecision.kind = deny
if validation.Reason == nil {
policyDecision.reason = metav1.StatusReasonInvalid
} else {
policyDecision.reason = *validation.Reason
}
if len(validation.Message) > 0 {
policyDecision.message = strings.TrimSpace(validation.Message)
} else {
policyDecision.message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
} else {
policyDecision.kind = admit
}
}
return decisions, nil
}
func createAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
// FIXME: how to get resource GVK, GVR and subresource?
gvk := attr.GetKind()
gvr := attr.GetResource()
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}

View File

@ -0,0 +1,572 @@
/*
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 (
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
func TestCompile(t *testing.T) {
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
errorExpressions map[string]string
}{
{
name: "invalid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
ParamKind: &v1alpha1.ParamKind{
APIVersion: "rules.example.com/v1",
Kind: "ReplicaLimit",
},
Validations: []v1alpha1.Validation{
{
Expression: "1 < 'asdf'",
},
{
Expression: "1 < 2",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
errorExpressions: map[string]string{
"1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)",
},
},
{
name: "valid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
Validations: []v1alpha1.Validation{
{
Expression: "1 < 2",
},
{
Expression: "object.spec.string.matches('[0-9]+')",
},
{
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var c CELValidatorCompiler
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
meets := make([]bool, len(validations))
for expr, expectErr := range tc.errorExpressions {
for i, result := range CompilationResults {
if validations[i].Expression == expr {
if result.Error == nil {
t.Errorf("Expect expression '%s' to contain error '%v' but got no error", expr, expectErr)
} else if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
meets[i] = true
}
}
}
for i, meet := range meets {
if !meet && CompilationResults[i].Error != nil {
t.Errorf("Unexpected err '%v' for expression '%s'", CompilationResults[i].Error, validations[i].Expression)
}
}
})
}
}
func getValidPolicy(validations []v1alpha1.Validation, params *v1alpha1.ParamKind, fp *v1alpha1.FailurePolicyType) *v1alpha1.ValidatingAdmissionPolicy {
if fp == nil {
fp = func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}()
}
return &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: fp,
Validations: validations,
ParamKind: params,
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
}
}
func generatedDecision(k policyDecisionKind, m string, r metav1.StatusReason) policyDecision {
return policyDecision{kind: k, message: m, reason: r}
}
func TestValidate(t *testing.T) {
// we fake the paramKind in ValidatingAdmissionPolicy for testing since the params is directly passed from cel admission
// Inside validator.go, we only check if paramKind exists
hasParamKind := &v1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
ignorePolicy := func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Ignore")
return &r
}()
forbiddenReason := func() *metav1.StatusReason {
r := metav1.StatusReasonForbidden
return &r
}()
configMapParams := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
}
crdParams := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"testSize": 10,
},
},
}
podObject := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: corev1.PodSpec{
NodeName: "testnode",
},
}
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
attributes admission.Attributes
params runtime.Object
policyDecisions []policyDecision
}{
{
name: "valid syntax for object",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for metadata",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.metadata.name == 'endpoints1'",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for oldObject",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
},
{
Expression: "object != null",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for request",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.operation == 'CREATE'"},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for configMap",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.namespace != params.data.fakeString"},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "test failure policy with Ignore",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "object.subsets.size() > 2"},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
},
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test failure policy with multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "has(object.subsets)",
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test failure policy with multiple failed validations",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject != null",
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: oldObject != null", metav1.StatusReasonInvalid),
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test Object nul in delete",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject != null",
},
{
Expression: "object == null",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(admit, "", ""),
},
},
{
name: "test reason for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: oldObject == null", metav1.StatusReasonForbidden),
},
},
{
name: "test message for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
Message: "old object should be present",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "old object should be present", metav1.StatusReasonForbidden),
},
},
{
name: "test runtime error",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject.x == 100",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "resulted in error", ""),
},
},
{
name: "test against crd param",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.subsets.size() < params.spec.testSize",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "test compile failure with FailurePolicy Fail",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "fail to compile test",
},
{
Expression: "object.subsets.size() > params.spec.testSize",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "compilation error: compilation failed: ERROR: <input>:1:6: Syntax error:", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid),
},
},
{
name: "test compile failure with FailurePolicy Ignore",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "fail to compile test",
},
{
Expression: "object.subsets.size() > params.spec.testSize",
},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "compilation error: compilation failed: ERROR:", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid),
},
},
{
name: "test pod",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.spec.nodeName == 'testnode'",
},
}, nil, nil),
attributes: newValidAttribute(&podObject, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := CELValidatorCompiler{}
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
policyResults, err := validator.Validate(tc.attributes, newObjectInterfacesForTest(), tc.params, tc.attributes.GetKind())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
require.Equal(t, len(policyResults), len(tc.policyDecisions))
for i, policyDecision := range tc.policyDecisions {
if policyDecision.kind != policyResults[i].kind {
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.kind, policyResults[i].kind)
}
if !strings.Contains(policyResults[i].message, policyDecision.message) {
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.message, policyResults[i].message)
}
if policyDecision.reason != policyResults[i].reason {
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.reason, policyResults[i].reason)
}
}
})
}
}
// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
func newObjectInterfacesForTest() admission.ObjectInterfaces {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
return admission.NewObjectInterfacesFromScheme(scheme)
}
func newValidAttribute(object runtime.Object, isDelete bool) admission.Attributes {
var oldObject runtime.Object
if !isDelete {
if object == nil {
object = &corev1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "endpoints1",
},
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
} else {
object = nil
oldObject = &corev1.Endpoints{
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
return admission.NewAttributesRecord(object, oldObject, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
}

View File

@ -219,7 +219,7 @@ func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { r
// Dispatch is called by the downstream Validate or Admit methods.
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
if rules.IsWebhookConfigurationResource(attr) {
if rules.IsExemptAdmissionConfigurationResource(attr) {
return nil
}
if !a.WaitForReady() {

View File

@ -116,12 +116,12 @@ func (r *Matcher) resource() bool {
return false
}
// IsWebhookConfigurationResource determines if an admission.Attributes object is describing
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration
func IsWebhookConfigurationResource(attr admission.Attributes) bool {
// IsExemptAdmissionConfigurationResource determines if an admission.Attributes object is describing
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration or a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
gvk := attr.GetKind()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" {
return true
}
}

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -86,7 +87,7 @@ func NewAdmissionOptions() *AdmissionOptions {
// admission plugins. The apiserver always runs the validating ones
// after all the mutating ones, so their relative order in this list
// doesn't matter.
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName},
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, cel.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: sets.NewString(),
}
server.RegisterAllAdmissionPlugins(options.Plugins)

View File

@ -36,7 +36,7 @@ func TestEnabledPluginNames(t *testing.T) {
}{
// scenario 0: check if a call to enabledPluginNames sets expected values.
{
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionWebhook"},
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"},
},
// scenario 1: use default off plugins if no specified

View File

@ -19,6 +19,7 @@ package server
// This file exists to force the desired plugin implementations to be linked into genericapi pkg.
import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins)
validatingwebhook.Register(plugins)
mutatingwebhook.Register(plugins)
cel.Register(plugins)
}

View File

@ -138,10 +138,12 @@ var (
// admissionExemptResources lists objects which are exempt from admission validation/mutation,
// only resources exempted from admission processing by API server should be listed here.
admissionExemptResources = map[schema.GroupVersionResource]bool{
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicybindings"): true,
}
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{