mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 20:24:09 +00:00
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:
parent
d86cfa9854
commit
e7d83a1fb7
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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...),
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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] = ¶mInfo{
|
c.paramsCRDControllers[*paramSource] = ¶mInfo{
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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{
|
||||||
|
Loading…
Reference in New Issue
Block a user