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

View File

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

View File

@ -75,7 +75,7 @@ var (
initEnvErr error 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. // If any changes are made here, consider to make the same changes there as well.
func getBaseEnv() (*cel.Env, error) { func getBaseEnv() (*cel.Env, error) {
initEnvOnce.Do(func() { initEnvOnce.Do(func() {

View File

@ -21,9 +21,16 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
"time" "time"
"k8s.io/apiserver/pkg/admission" "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" "k8s.io/client-go/tools/cache"
) )
@ -38,7 +45,7 @@ import (
const ( const (
// PluginName indicates the name of admission plug-in // PluginName indicates the name of admission plug-in
PluginName = "CEL" PluginName = "ValidatingAdmissionPolicy"
) )
// Register registers a plugin // Register registers a plugin
@ -54,28 +61,89 @@ func Register(plugins *admission.Plugins) {
type celAdmissionPlugin struct { type celAdmissionPlugin struct {
evaluator CELPolicyEvaluator 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{} var _ admission.ValidationInterface = &celAdmissionPlugin{}
func NewPlugin() (*celAdmissionPlugin, error) { func NewPlugin() (admission.Interface, error) {
result := &celAdmissionPlugin{} result := &celAdmissionPlugin{}
return result, nil return result, nil
} }
func (c *celAdmissionPlugin) SetCELPolicyEvaluator(evaluator CELPolicyEvaluator) { func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.evaluator = evaluator c.informerFactory = f
} }
// Once clientset and informer factory are provided, creates and starts the func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
// admission controller 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 { 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 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, a admission.Attributes,
o admission.ObjectInterfaces, o admission.ObjectInterfaces,
) (err error) { ) (err error) {
if !c.enabled {
return nil
}
deadlined, cancel := context.WithTimeout(ctx, 2*time.Second) deadlined, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel() 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) { if !cache.WaitForNamedCacheSync("cel-admission-plugin", deadlined.Done(), c.evaluator.HasSynced) {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request")) return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
} }
return c.evaluator.Validate(ctx, a, o) 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, prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize), cel.EvalOptions(cel.OptOptimize),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...), cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),

View File

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

View File

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

View File

@ -17,92 +17,34 @@ limitations under the License.
package cel package cel
import ( import (
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"github.com/google/cel-go/common/types/ref"
) )
type FailurePolicy string // Validator defines the func used to validate the cel expressions
// matchKind provides the GroupVersionKind that the object should be
const ( // validated by CEL expressions as.
Fail FailurePolicy = "Fail" type Validator interface {
Ignore FailurePolicy = "Ignore" Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error)
)
// 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)
} }
// PolicyDefinition is an interface for internal policy binding type. // ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile`
// Implemented by mock/testing types, and to be implemented by the public API // function to assist with converting types and values to/from CEL-typed values.
// types once they have completed API review. type ValidatorCompiler interface {
// admission.InitializationValidator
// The interface closely mirrors the format and functionality of the
// PolicyDefinition proposed in the KEP.
type PolicyDefinition interface {
runtime.Object
// Matches says whether this policy definition matches the provided admission // Matches says whether this policy definition matches the provided admission
// resource request // resource request
Matches(a admission.Attributes) bool DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
Compile( // Matches says whether this policy definition matches the provided admission
// 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
// resource request // 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 // Compile is used for the cel expression compilation
// by this binding. Compile(
GetTargetDefinition() (namespace, name string) policy *v1alpha1.ValidatingAdmissionPolicy,
) Validator
// 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)
} }

View File

@ -52,7 +52,7 @@ type ControllerOptions struct {
Workers uint Workers uint
} }
func (c controller[T]) Informer() Informer[T] { func (c *controller[T]) Informer() Informer[T] {
return c.informer return c.informer
} }
@ -73,7 +73,7 @@ func NewController[T runtime.Object](
options: options, options: options,
informer: informer, informer: informer,
reconciler: reconciler, 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) klog.Infof("starting %s", c.options.Name)
defer klog.Infof("stopping %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{}) { enqueue := func(obj interface{}) {
var key string var key string
var err error var err error
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil { if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err) utilruntime.HandleError(err)
return return
} }
@ -185,7 +193,7 @@ func (c *controller[T]) HasSynced() bool {
func (c *controller[T]) runWorker() { func (c *controller[T]) runWorker() {
for { for {
obj, shutdown := c.queue.Get() key, shutdown := c.queue.Get()
if shutdown { if shutdown {
return return
} }
@ -221,9 +229,9 @@ func (c *controller[T]) runWorker() {
// Finally, if no error occurs we Forget this item so it is allowed // Finally, if no error occurs we Forget this item so it is allowed
// to be re-enqueued without a long rate limit // to be re-enqueued without a long rate limit
c.queue.Forget(obj) c.queue.Forget(obj)
klog.Infof("Successfully synced '%s'", key) klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
return nil return nil
}(obj) }(key)
if err != nil { if err != nil {
utilruntime.HandleError(err) utilruntime.HandleError(err)

View File

@ -17,37 +17,43 @@ limitations under the License.
package cel package cel
import ( import (
"encoding/json" "net/http"
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type PolicyDecisionKind string type policyDecisionKind string
const ( const (
Admit PolicyDecisionKind = "Admit" admit policyDecisionKind = "admit"
Deny PolicyDecisionKind = "Deny" deny policyDecisionKind = "deny"
) )
type PolicyDecision struct { type policyDecision struct {
Kind PolicyDecisionKind `json:"kind"` kind policyDecisionKind
Message any `json:"message"` message string
reason metav1.StatusReason
} }
type PolicyDecisionWithMetadata struct { type policyDecisionWithMetadata struct {
PolicyDecision `json:"decision"` policyDecision
Definition PolicyDefinition `json:"definition"` definition *v1alpha1.ValidatingAdmissionPolicy
Binding PolicyBinding `json:"binding"` binding *v1alpha1.ValidatingAdmissionPolicyBinding
} }
type PolicyError struct { func reasonToCode(r metav1.StatusReason) int32 {
Decisions []PolicyDecisionWithMetadata switch r {
} case metav1.StatusReasonForbidden:
return http.StatusForbidden
func (p *PolicyError) Error() string { case metav1.StatusReasonUnauthorized:
// Just format the error as JSON return http.StatusUnauthorized
jsonText, err := json.Marshal(p.Decisions) case metav1.StatusReasonRequestEntityTooLarge:
if err != nil { return http.StatusRequestEntityTooLarge
return fmt.Sprintf("error formatting PolicyError: %s", err.Error()) 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. // Dispatch is called by the downstream Validate or Admit methods.
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error { func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
if rules.IsWebhookConfigurationResource(attr) { if rules.IsExemptAdmissionConfigurationResource(attr) {
return nil return nil
} }
if !a.WaitForReady() { if !a.WaitForReady() {

View File

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

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/initializer"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" "k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -86,7 +87,7 @@ func NewAdmissionOptions() *AdmissionOptions {
// admission plugins. The apiserver always runs the validating ones // admission plugins. The apiserver always runs the validating ones
// after all the mutating ones, so their relative order in this list // after all the mutating ones, so their relative order in this list
// doesn't matter. // doesn't matter.
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName}, RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, cel.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: sets.NewString(), DefaultOffPlugins: sets.NewString(),
} }
server.RegisterAllAdmissionPlugins(options.Plugins) 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. // 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 // 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. // This file exists to force the desired plugin implementations to be linked into genericapi pkg.
import ( import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle" "k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating" mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating" validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins) lifecycle.Register(plugins)
validatingwebhook.Register(plugins) validatingwebhook.Register(plugins)
mutatingwebhook.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, // admissionExemptResources lists objects which are exempt from admission validation/mutation,
// only resources exempted from admission processing by API server should be listed here. // only resources exempted from admission processing by API server should be listed here.
admissionExemptResources = map[schema.GroupVersionResource]bool{ admissionExemptResources = map[schema.GroupVersionResource]bool{
gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true, gvr("admissionregistration.k8s.io", "v1beta1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true, gvr("admissionregistration.k8s.io", "v1beta1", "validatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true, gvr("admissionregistration.k8s.io", "v1", "mutatingwebhookconfigurations"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingwebhookconfigurations"): 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{ parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{