refactor: implement VAP off of policy plugin fw

This commit is contained in:
Alexander Zielenski 2024-01-22 17:31:52 -08:00
parent a6366573d5
commit 18fbc48b01
7 changed files with 702 additions and 1959 deletions

View File

@ -0,0 +1,95 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewValidatingAdmissionPolicyAccessor(obj *v1beta1.ValidatingAdmissionPolicy) generic.PolicyAccessor {
return &validatingAdmissionPolicyAccessor{
ValidatingAdmissionPolicy: obj,
}
}
func NewValidatingAdmissionPolicyBindingAccessor(obj *v1beta1.ValidatingAdmissionPolicyBinding) generic.BindingAccessor {
return &validatingAdmissionPolicyBindingAccessor{
ValidatingAdmissionPolicyBinding: obj,
}
}
type validatingAdmissionPolicyAccessor struct {
*v1beta1.ValidatingAdmissionPolicy
}
func (v *validatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *validatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *validatingAdmissionPolicyAccessor) GetParamKind() *schema.GroupVersionKind {
paramKind := v.Spec.ParamKind
if paramKind == nil {
return nil
}
groupVersion, err := schema.ParseGroupVersion(paramKind.APIVersion)
if err != nil {
// A validatingadmissionpolicy which passes validation should have
// a parseable APIVersion for its ParamKind, so this should never happen
// if the policy is valid.
//
// Return a bogus but non-nil GVK that will throw an error about the
// invalid APIVersion when the param is looked up.
return &schema.GroupVersionKind{
Group: paramKind.APIVersion,
Version: "",
Kind: paramKind.Kind,
}
}
return &schema.GroupVersionKind{
Group: groupVersion.Group,
Version: groupVersion.Version,
Kind: paramKind.Kind,
}
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1beta1.ValidatingAdmissionPolicyBinding
}
func (v *validatingAdmissionPolicyBindingAccessor) GetNamespace() string {
return v.Namespace
}
func (v *validatingAdmissionPolicyBindingAccessor) GetName() string {
return v.Name
}
func (v *validatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
return types.NamespacedName{
Namespace: "",
Name: v.Spec.PolicyName,
}
}

View File

@ -1,197 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"context"
"errors"
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
////////////////////////////////////////////////////////////////////////////////
// Plugin Definition
////////////////////////////////////////////////////////////////////////////////
// Definition for CEL admission plugin. This is the entry point into the
// CEL admission control system.
//
// Each plugin is asked to validate every object update.
const (
// PluginName indicates the name of admission plug-in
PluginName = "ValidatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return NewPlugin()
})
}
////////////////////////////////////////////////////////////////////////////////
// Plugin Initialization & Dependency Injection
////////////////////////////////////////////////////////////////////////////////
type celAdmissionPlugin struct {
*admission.Handler
evaluator CELPolicyEvaluator
inspectedFeatureGates bool
enabled bool
// Injected Dependencies
informerFactory informers.SharedInformerFactory
client kubernetes.Interface
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
stopCh <-chan struct{}
authorizer authorizer.Authorizer
}
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
var _ admission.InitializationValidator = &celAdmissionPlugin{}
var _ admission.ValidationInterface = &celAdmissionPlugin{}
func NewPlugin() (admission.Interface, error) {
return &celAdmissionPlugin{
Handler: admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update),
}, nil
}
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.informerFactory = f
}
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
c.client = client
}
func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
c.restMapper = mapper
}
func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) {
c.dynamicClient = client
}
func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
c.authorizer = authorizer
}
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
c.enabled = true
}
c.inspectedFeatureGates = true
}
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *celAdmissionPlugin) ValidateInitialization() error {
if !c.inspectedFeatureGates {
return fmt.Errorf("%s did not see feature gates", PluginName)
}
if !c.enabled {
return nil
}
if c.informerFactory == nil {
return errors.New("missing informer factory")
}
if c.client == nil {
return errors.New("missing kubernetes client")
}
if c.restMapper == nil {
return errors.New("missing rest mapper")
}
if c.dynamicClient == nil {
return errors.New("missing dynamic client")
}
if c.stopCh == nil {
return errors.New("missing stop channel")
}
if c.authorizer == nil {
return errors.New("missing authorizer")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
if err := c.evaluator.ValidateInitialization(); err != nil {
return err
}
c.SetReadyFunc(c.evaluator.HasSynced)
go c.evaluator.Run(c.stopCh)
return nil
}
////////////////////////////////////////////////////////////////////////////////
// admission.ValidationInterface
////////////////////////////////////////////////////////////////////////////////
func (c *celAdmissionPlugin) Handles(operation admission.Operation) bool {
return true
}
func (c *celAdmissionPlugin) Validate(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.enabled {
return nil
}
// isPolicyResource determines if an admission.Attributes object is describing
// the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
if isPolicyResource(a) {
return
}
if !c.WaitForReady() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
return c.evaluator.Validate(ctx, a, o)
}
func isPolicyResource(attr admission.Attributes) bool {
gvk := attr.GetResource()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
return true
}
}
return false
}

View File

@ -1,551 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"context"
"fmt"
"sync"
"time"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
type policyController struct {
once sync.Once
context context.Context
dynamicClient dynamic.Interface
informerFactory informers.SharedInformerFactory
restMapper meta.RESTMapper
policyDefinitionsController generic.Controller[*v1beta1.ValidatingAdmissionPolicy]
policyBindingController generic.Controller[*v1beta1.ValidatingAdmissionPolicyBinding]
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
// pass nil to create filter compiler in demand
filterCompiler cel.FilterCompiler
matcher Matcher
newValidator
client kubernetes.Interface
// Lock which protects
// All Below fields
// All above fields should be assumed constant
mutex sync.RWMutex
cachedPolicies []policyData
// controller and metadata
paramsCRDControllers map[v1beta1.ParamKind]*paramInfo
// Index for each definition namespace/name, contains all binding
// namespace/names known to exist for that definition
definitionInfo map[namespacedName]*definitionInfo
// Index for each bindings namespace/name. Contains compiled templates
// for the binding depending on the policy/param combination.
bindingInfos map[namespacedName]*bindingInfo
// Map from namespace/name of a definition to a set of namespace/name
// of bindings which depend on it.
// All keys must have at least one dependent binding
// All binding names MUST exist as a key bindingInfos
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
}
type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType) Validator
func newPolicyController(
restMapper meta.RESTMapper,
client kubernetes.Interface,
dynamicClient dynamic.Interface,
informerFactory informers.SharedInformerFactory,
filterCompiler cel.FilterCompiler,
matcher Matcher,
policiesInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicy],
bindingsInformer generic.Informer[*v1beta1.ValidatingAdmissionPolicyBinding],
) *policyController {
res := &policyController{}
*res = policyController{
filterCompiler: filterCompiler,
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1beta1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
matcher: matcher,
newValidator: NewValidator,
policyDefinitionsController: generic.NewController(
policiesInformer,
res.reconcilePolicyDefinition,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-definitions",
},
),
policyBindingController: generic.NewController(
bindingsInformer,
res.reconcilePolicyBinding,
generic.ControllerOptions{
Workers: 1,
Name: "cel-policy-bindings",
},
),
restMapper: restMapper,
dynamicClient: dynamicClient,
informerFactory: informerFactory,
client: client,
}
return res
}
func (c *policyController) Run(ctx context.Context) {
// Only support being run once
c.once.Do(func() {
c.context = ctx
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
c.policyDefinitionsController.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
c.policyBindingController.Run(ctx)
}()
<-ctx.Done()
wg.Wait()
})
}
func (c *policyController) HasSynced() bool {
return c.policyDefinitionsController.HasSynced() && c.policyBindingController.HasSynced()
}
func (c *policyController) reconcilePolicyDefinition(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
c.mutex.Lock()
defer c.mutex.Unlock()
err := c.reconcilePolicyDefinitionSpec(namespace, name, definition)
return err
}
func (c *policyController) reconcilePolicyDefinitionSpec(namespace, name string, definition *v1beta1.ValidatingAdmissionPolicy) error {
c.cachedPolicies = nil // invalidate cachedPolicies
// Namespace for policydefinition is empty.
nn := getNamespaceName(namespace, name)
info, ok := c.definitionInfo[nn]
if !ok {
info = &definitionInfo{}
c.definitionInfo[nn] = info
// TODO(DangerOnTheRanger): add support for "warn" being a valid enforcementAction
celmetrics.Metrics.ObserveDefinition(context.TODO(), "active", "deny")
}
// Skip reconcile if the spec of the definition is unchanged and had a
// successful previous sync
if info.configurationError == nil && info.lastReconciledValue != nil && definition != nil &&
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, definition.Spec) {
return nil
}
var paramSource *v1beta1.ParamKind
if definition != nil {
paramSource = definition.Spec.ParamKind
}
// If param source has changed, remove definition as dependent of old params
// If there are no more dependents of old param, stop and clean up controller
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
// If we are:
// - switching from having a param to not having a param (includes deletion)
// - or from having a param to a different one
// we remove dependency on the controller.
if paramSource == nil || *paramSource != oldParamSource {
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
oldParamInfo.dependentDefinitions.Delete(nn)
if len(oldParamInfo.dependentDefinitions) == 0 {
oldParamInfo.stop()
delete(c.paramsCRDControllers, oldParamSource)
}
}
}
}
// Reset all previously compiled evaluators in case something relevant in
// definition has changed.
for key := range c.definitionsToBindings[nn] {
bindingInfo := c.bindingInfos[key]
bindingInfo.validator = nil
c.bindingInfos[key] = bindingInfo
}
if definition == nil {
delete(c.definitionInfo, nn)
return nil
}
// Update definition info
info.lastReconciledValue = definition
info.configurationError = nil
if paramSource == nil {
// Skip setting up controller for empty param type
return nil
}
// find GVR for params
// Parse param source into a GVK
paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
// Return nil, since this error cannot be resolved by waiting more time
return nil
}
paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
Group: paramSourceGV.Group,
Kind: paramSource.Kind,
}, paramSourceGV.Version)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
//
info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
return info.configurationError
}
paramInfo := c.ensureParamInfo(paramSource, paramsGVR)
paramInfo.dependentDefinitions.Insert(nn)
return nil
}
// Ensures that there is an informer started for the given GVK to be used as a
// param
func (c *policyController) ensureParamInfo(paramSource *v1beta1.ParamKind, mapping *meta.RESTMapping) *paramInfo {
if info, ok := c.paramsCRDControllers[*paramSource]; ok {
return info
}
// We are not watching this param. Start an informer for it.
instanceContext, instanceCancel := context.WithCancel(c.context)
var informer cache.SharedIndexInformer
// Try to see if our provided informer factory has an informer for this type.
// We assume the informer is already started, and starts all types associated
// with it.
if genericInformer, err := c.informerFactory.ForResource(mapping.Resource); err == nil {
informer = genericInformer.Informer()
// Ensure the informer is started
// Use policyController's context rather than the instance context.
// PolicyController context is expected to last until app shutdown
// This is due to behavior of informerFactory which would cause the
// informer to stop running once the context is cancelled, and
// never started again.
c.informerFactory.Start(c.context.Done())
} else {
// Dynamic JSON informer fallback.
// Cannot use shared dynamic informer since it would be impossible
// to clean CRD informers properly with multiple dependents
// (cannot start ahead of time, and cannot track dependencies via stopCh)
informer = dynamicinformer.NewFilteredDynamicInformer(
c.dynamicClient,
mapping.Resource,
corev1.NamespaceAll,
// Use same interval as is used for k8s typed sharedInformerFactory
// https://github.com/kubernetes/kubernetes/blob/7e0923899fed622efbc8679cca6b000d43633e38/cmd/kube-apiserver/app/server.go#L430
10*time.Minute,
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
nil,
).Informer()
go informer.Run(instanceContext.Done())
}
controller := generic.NewController(
generic.NewInformer[runtime.Object](informer),
c.reconcileParams,
generic.ControllerOptions{
Workers: 1,
Name: paramSource.String() + "-controller",
},
)
ret := &paramInfo{
controller: controller,
stop: instanceCancel,
scope: mapping.Scope,
dependentDefinitions: sets.New[namespacedName](),
}
c.paramsCRDControllers[*paramSource] = ret
go controller.Run(instanceContext)
return ret
}
func (c *policyController) reconcilePolicyBinding(namespace, name string, binding *v1beta1.ValidatingAdmissionPolicyBinding) error {
c.mutex.Lock()
defer c.mutex.Unlock()
c.cachedPolicies = nil // invalidate cachedPolicies
// Namespace for PolicyBinding is empty. In the future a namespaced binding
// may be added
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
nn := getNamespaceName(namespace, name)
info, ok := c.bindingInfos[nn]
if !ok {
info = &bindingInfo{}
c.bindingInfos[nn] = info
}
// Skip if the spec of the binding is unchanged.
if info.lastReconciledValue != nil && binding != nil &&
apiequality.Semantic.DeepEqual(info.lastReconciledValue.Spec, binding.Spec) {
return nil
}
var oldNamespacedDefinitionName namespacedName
if info.lastReconciledValue != nil {
// All validating policies are cluster-scoped so have empty namespace
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
}
var namespacedDefinitionName namespacedName
if binding != nil {
// All validating policies are cluster-scoped so have empty namespace
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
}
// Remove record of binding from old definition if the referred policy
// has changed
if oldNamespacedDefinitionName != namespacedDefinitionName {
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
dependentBindings.Delete(nn)
// if there are no more dependent bindings, remove knowledge of the
// definition altogether
if len(dependentBindings) == 0 {
delete(c.definitionsToBindings, oldNamespacedDefinitionName)
}
}
}
if binding == nil {
delete(c.bindingInfos, nn)
return nil
}
// Add record of binding to new definition
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
dependentBindings.Insert(nn)
} else {
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
}
// Remove compiled template for old binding
info.validator = nil
info.lastReconciledValue = binding
return nil
}
func (c *policyController) reconcileParams(namespace, name string, params runtime.Object) error {
// Do nothing.
// When we add informational type checking we will need to compile in the
// reconcile loops instead of lazily so we can add compiler errors / type
// checker errors to the status of the resources.
return nil
}
// Fetches the latest set of policy data or recalculates it if it has changed
// since it was last fetched
func (c *policyController) latestPolicyData() []policyData {
existing := func() []policyData {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.cachedPolicies
}()
if existing != nil {
return existing
}
c.mutex.Lock()
defer c.mutex.Unlock()
var res []policyData
for definitionNN, definitionInfo := range c.definitionInfo {
var bindingInfos []bindingInfo
for bindingNN := range c.definitionsToBindings[definitionNN] {
bindingInfo := c.bindingInfos[bindingNN]
if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
hasParam := false
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
hasParam = true
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy)
var matcher matchconditions.Matcher = nil
matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions
filterCompiler := c.filterCompiler
if filterCompiler == nil {
compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
if err == nil {
filterCompiler = compositedCompiler
compositedCompiler.CompileAndStoreVariables(convertv1beta1Variables(definitionInfo.lastReconciledValue.Spec.Variables), optionalVars, environment.StoredExpressions)
} else {
utilruntime.HandleError(err)
}
}
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", definitionInfo.lastReconciledValue.Name)
}
bindingInfo.validator = c.newValidator(
filterCompiler.Compile(convertv1beta1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1beta1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1beta1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)
}
bindingInfos = append(bindingInfos, *bindingInfo)
}
var pInfo paramInfo
if paramKind := definitionInfo.lastReconciledValue.Spec.ParamKind; paramKind != nil {
if info, ok := c.paramsCRDControllers[*paramKind]; ok {
pInfo = *info
}
}
res = append(res, policyData{
definitionInfo: *definitionInfo,
paramInfo: pInfo,
bindings: bindingInfos,
})
}
c.cachedPolicies = res
return res
}
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
if policyType == nil {
return nil
}
var v1FailPolicy v1.FailurePolicyType
if *policyType == v1beta1.Fail {
v1FailPolicy = v1.Fail
} else if *policyType == v1beta1.Ignore {
v1FailPolicy = v1.Ignore
}
return &v1FailPolicy
}
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := ValidationCondition{
Expression: validation.Expression,
Message: validation.Message,
Reason: validation.Reason,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
if validation.MessageExpression != "" {
condition := MessageExpressionCondition{
MessageExpression: validation.MessageExpression,
}
celExpressionAccessor[i] = &condition
}
}
return celExpressionAccessor
}
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := AuditAnnotationCondition{
Key: validation.Key,
ValueExpression: validation.ValueExpression,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor {
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
for i, variable := range variables {
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
}
return namedExpressions
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,
name: name,
}
}

View File

@ -21,8 +21,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"sync/atomic"
"time" "time"
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/api/admissionregistration/v1beta1"
@ -34,47 +32,32 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json" utiljson "k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/policy/internal/generic" "k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics" celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
celconfig "k8s.io/apiserver/pkg/apis/cel" celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/warning" "k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/klog/v2" "k8s.io/klog/v2"
) )
var _ CELPolicyEvaluator = &celAdmissionController{} type dispatcher struct {
matcher Matcher
// celAdmissionController is the top-level controller for admission control using CEL authz authorizer.Authorizer
// it is responsible for watching policy definitions, bindings, and config param CRDs
type celAdmissionController struct {
// Controller which manages book-keeping for the cluster's dynamic policy
// information.
policyController *policyController
// atomic []policyData
// list of every known policy definition, and all informatoin required to
// validate its bindings against an object.
// A snapshot of the current policy configuration is synced with this field
// asynchronously
definitions atomic.Value
authz authorizer.Authorizer
} }
// Everything someone might need to validate a single ValidatingPolicyDefinition var _ generic.Dispatcher[PolicyHook] = &dispatcher{}
// against all of its registered bindings.
type policyData struct { func NewDispatcher(
definitionInfo authorizer authorizer.Authorizer,
paramInfo matcher Matcher,
bindings []bindingInfo ) generic.Dispatcher[PolicyHook] {
return &dispatcher{
matcher: matcher,
authz: authorizer,
}
} }
// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding // contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding
@ -85,110 +68,8 @@ type policyDecisionWithMetadata struct {
Binding *v1beta1.ValidatingAdmissionPolicyBinding Binding *v1beta1.ValidatingAdmissionPolicyBinding
} }
// namespaceName is used as a key in definitionInfo and bindingInfos // Dispatch implements generic.Dispatcher.
type namespacedName struct { func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
namespace, name string
}
type definitionInfo struct {
// Error about the state of the definition's configuration and the cluster
// preventing its enforcement or compilation.
// Reset every reconciliation
configurationError error
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue *v1beta1.ValidatingAdmissionPolicy
}
type bindingInfo struct {
// Compiled CEL expression turned into an validator
validator Validator
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue *v1beta1.ValidatingAdmissionPolicyBinding
}
type paramInfo struct {
// Controller which is watching this param CRD
controller generic.Controller[runtime.Object]
// Function to call to stop the informer and clean up the controller
stop func()
// Whether this param is cluster or namespace scoped
scope meta.RESTScope
// Policy Definitions which refer to this param CRD
dependentDefinitions sets.Set[namespacedName]
}
func NewAdmissionController(
// Injected Dependencies
informerFactory informers.SharedInformerFactory,
client kubernetes.Interface,
restMapper meta.RESTMapper,
dynamicClient dynamic.Interface,
authz authorizer.Authorizer,
) CELPolicyEvaluator {
return &celAdmissionController{
definitions: atomic.Value{},
policyController: newPolicyController(
restMapper,
client,
dynamicClient,
informerFactory,
nil,
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
generic.NewInformer[*v1beta1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer()),
generic.NewInformer[*v1beta1.ValidatingAdmissionPolicyBinding](
informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer()),
),
authz: authz,
}
}
func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
c.policyController.Run(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
// Wait indefinitely until policies/bindings are listed & handled before
// allowing policies to be refreshed
if !cache.WaitForNamedCacheSync("cel-admission-controller", ctx.Done(), c.policyController.HasSynced) {
return
}
// Loop every 1 second until context is cancelled, refreshing policies
wait.Until(c.refreshPolicies, 1*time.Second, ctx.Done())
}()
<-stopCh
cancel()
wg.Wait()
}
const maxAuditAnnotationValueLength = 10 * 1024
func (c *celAdmissionController) Validate(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.HasSynced() {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
var deniedDecisions []policyDecisionWithMetadata var deniedDecisions []policyDecisionWithMetadata
@ -232,19 +113,18 @@ func (c *celAdmissionController) Validate(
}) })
} }
} }
policyDatas := c.definitions.Load().([]policyData)
authz := newCachingAuthorizer(c.authz) authz := newCachingAuthorizer(c.authz)
for _, definitionInfo := range policyDatas { for _, hook := range hooks {
// versionedAttributes will be set to non-nil inside of the loop, but // versionedAttributes will be set to non-nil inside of the loop, but
// is scoped outside of the param loop so we only convert once. We defer // is scoped outside of the param loop so we only convert once. We defer
// conversion so that it is only performed when we know a policy matches, // conversion so that it is only performed when we know a policy matches,
// saving the cost of converting non-matching requests. // saving the cost of converting non-matching requests.
var versionedAttr *admission.VersionedAttributes var versionedAttr *admission.VersionedAttributes
definition := definitionInfo.lastReconciledValue definition := hook.Policy
matches, matchResource, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition) matches, matchResource, matchKind, err := c.matcher.DefinitionMatches(a, o, definition)
if err != nil { if err != nil {
// Configuration error. // Configuration error.
addConfigError(err, definition, nil) addConfigError(err, definition, nil)
@ -253,18 +133,17 @@ func (c *celAdmissionController) Validate(
if !matches { if !matches {
// Policy definition does not match request // Policy definition does not match request
continue continue
} else if definitionInfo.configurationError != nil { } else if hook.ConfigurationError != nil {
// Configuration error. // Configuration error.
addConfigError(definitionInfo.configurationError, definition, nil) addConfigError(hook.ConfigurationError, definition, nil)
continue continue
} }
auditAnnotationCollector := newAuditAnnotationCollector() auditAnnotationCollector := newAuditAnnotationCollector()
for _, bindingInfo := range definitionInfo.bindings { for _, binding := range hook.Bindings {
// If the key is inside dependentBindings, there is guaranteed to // If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it // be a bindingInfo for it
binding := bindingInfo.lastReconciledValue matches, err := c.matcher.BindingMatches(a, o, binding)
matches, err := c.policyController.matcher.BindingMatches(a, o, binding)
if err != nil { if err != nil {
// Configuration error. // Configuration error.
addConfigError(err, definition, binding) addConfigError(err, definition, binding)
@ -274,7 +153,14 @@ func (c *celAdmissionController) Validate(
continue continue
} }
params, err := c.collectParams(definition.Spec.ParamKind, definitionInfo.paramInfo, binding.Spec.ParamRef, a.GetNamespace()) params, err := c.collectParams(
definition.Spec.ParamKind,
hook.ParamInformer,
hook.ParamScope,
binding.Spec.ParamRef,
a.GetNamespace(),
)
if err != nil { if err != nil {
addConfigError(err, definition, binding) addConfigError(err, definition, binding)
continue continue
@ -303,7 +189,7 @@ func (c *celAdmissionController) Validate(
// if it is cluster scoped, namespaceName will be empty // if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource. // Otherwise, get the Namespace resource.
if namespaceName != "" { if namespaceName != "" {
namespace, err = c.policyController.matcher.GetNamespace(namespaceName) namespace, err = c.matcher.GetNamespace(namespaceName)
if err != nil { if err != nil {
return err return err
} }
@ -323,7 +209,18 @@ func (c *celAdmissionController) Validate(
nested: param, nested: param,
} }
} }
validationResults = append(validationResults, bindingInfo.validator.Validate(ctx, matchResource, versionedAttr, p, namespace, celconfig.RuntimeCELCostBudget, authz))
validationResults = append(validationResults,
hook.Evaluator.Validate(
ctx,
matchResource,
versionedAttr,
p,
namespace,
celconfig.RuntimeCELCostBudget,
authz,
),
)
} }
for _, validationResult := range validationResults { for _, validationResult := range validationResults {
@ -344,7 +241,7 @@ func (c *celAdmissionController) Validate(
}) })
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active") celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1beta1.Audit: case v1beta1.Audit:
c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr) publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active") celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1beta1.Warn: case v1beta1.Warn:
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message)) warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
@ -411,28 +308,30 @@ func (c *celAdmissionController) Validate(
} }
// Returns objects to use to evaluate the policy // Returns objects to use to evaluate the policy
func (c *celAdmissionController) collectParams( // Copied with minor modification to account for slightly different arguments
func (c *dispatcher) collectParams(
paramKind *v1beta1.ParamKind, paramKind *v1beta1.ParamKind,
info paramInfo, paramInformer informers.GenericInformer,
paramScope meta.RESTScope,
paramRef *v1beta1.ParamRef, paramRef *v1beta1.ParamRef,
namespace string, namespace string,
) ([]runtime.Object, error) { ) ([]runtime.Object, error) {
// If definition has paramKind, paramRef is required in binding. // If definition has paramKind, paramRef is required in binding.
// If definition has no paramKind, paramRef set in binding will be ignored. // If definition has no paramKind, paramRef set in binding will be ignored.
var params []runtime.Object var params []runtime.Object
var paramStore generic.NamespacedLister[runtime.Object] var paramStore cache.GenericNamespaceLister
// Make sure the param kind is ready to use // Make sure the param kind is ready to use
if paramKind != nil && paramRef != nil { if paramKind != nil && paramRef != nil {
if info.controller == nil { if paramInformer == nil {
return nil, fmt.Errorf("paramKind kind `%v` not known", return nil, fmt.Errorf("paramKind kind `%v` not known",
paramKind.String()) paramKind.String())
} }
// Set up cluster-scoped, or namespaced access to the params // Set up cluster-scoped, or namespaced access to the params
// "default" if not provided, and paramKind is namespaced // "default" if not provided, and paramKind is namespaced
paramStore = info.controller.Informer() paramStore = paramInformer.Lister()
if info.scope.Name() == meta.RESTScopeNameNamespace { if paramScope.Name() == meta.RESTScopeNameNamespace {
paramsNamespace := namespace paramsNamespace := namespace
if len(paramRef.Namespace) > 0 { if len(paramRef.Namespace) > 0 {
paramsNamespace = paramRef.Namespace paramsNamespace = paramRef.Namespace
@ -442,16 +341,16 @@ func (c *celAdmissionController) collectParams(
return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources") return nil, fmt.Errorf("cannot use namespaced paramRef in policy binding that matches cluster-scoped resources")
} }
paramStore = info.controller.Informer().Namespaced(paramsNamespace) paramStore = paramInformer.Lister().ByNamespace(paramsNamespace)
} }
// If the param informer for this admission policy has not yet // If the param informer for this admission policy has not yet
// had time to perform an initial listing, don't attempt to use // had time to perform an initial listing, don't attempt to use
// it. // it.
timeoutCtx, cancel := context.WithTimeout(c.policyController.context, 1*time.Second) timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() defer cancel()
if !cache.WaitForCacheSync(timeoutCtx.Done(), info.controller.HasSynced) { if !cache.WaitForCacheSync(timeoutCtx.Done(), paramInformer.Informer().HasSynced) {
return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission", return nil, fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String()) paramKind.String())
} }
@ -467,7 +366,7 @@ func (c *celAdmissionController) collectParams(
// Policy ParamKind is set, but binding does not use it. // Policy ParamKind is set, but binding does not use it.
// Validate with nil params // Validate with nil params
return []runtime.Object{nil}, nil return []runtime.Object{nil}, nil
case len(paramRef.Namespace) > 0 && info.scope.Name() == meta.RESTScopeRoot.Name(): case len(paramRef.Namespace) > 0 && paramScope.Name() == meta.RESTScopeRoot.Name():
// Not allowed to set namespace for cluster-scoped param // Not allowed to set namespace for cluster-scoped param
return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`") return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
@ -527,10 +426,10 @@ func (c *celAdmissionController) collectParams(
return params, nil return params, nil
} }
func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) { func publishValidationFailureAnnotation(binding *v1beta1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
key := "validation.policy.admission.k8s.io/validation_failure" key := "validation.policy.admission.k8s.io/validation_failure"
// Marshal to a list of failures since, in the future, we may need to support multiple failures // Marshal to a list of failures since, in the future, we may need to support multiple failures
valueJson, err := utiljson.Marshal([]validationFailureValue{{ valueJSON, err := utiljson.Marshal([]ValidationFailureValue{{
ExpressionIndex: expressionIndex, ExpressionIndex: expressionIndex,
Message: decision.Message, Message: decision.Message,
ValidationActions: binding.Spec.ValidationActions, ValidationActions: binding.Spec.ValidationActions,
@ -540,27 +439,17 @@ func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1b
if err != nil { if err != nil {
klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err) klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
} }
value := string(valueJson) value := string(valueJSON)
if err := attributes.AddAnnotation(key, value); err != nil { if err := attributes.AddAnnotation(key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err) klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
} }
} }
func (c *celAdmissionController) HasSynced() bool { const maxAuditAnnotationValueLength = 10 * 1024
return c.policyController.HasSynced() && c.definitions.Load() != nil
}
func (c *celAdmissionController) ValidateInitialization() error {
return c.policyController.matcher.ValidateInitialization()
}
func (c *celAdmissionController) refreshPolicies() {
c.definitions.Store(c.policyController.latestPolicyData())
}
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit // validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
// annotation value. // annotation value.
type validationFailureValue struct { type ValidationFailureValue struct {
Message string `json:"message"` Message string `json:"message"`
Policy string `json:"policy"` Policy string `json:"policy"`
Binding string `json:"binding"` Binding string `json:"binding"`

View File

@ -0,0 +1,197 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validating
import (
"context"
"io"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
const (
// PluginName indicates the name of admission plug-in
PluginName = "ValidatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
return NewPlugin(configFile), nil
})
}
// Plugin is an implementation of admission.Interface.
type Policy = v1beta1.ValidatingAdmissionPolicy
type PolicyBinding = v1beta1.ValidatingAdmissionPolicyBinding
type PolicyEvaluator = Validator
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
type Plugin struct {
*generic.Plugin[PolicyHook]
}
var _ admission.Interface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{}
var _ initializer.WantsFeatures = &Plugin{}
func NewPlugin(_ io.Reader) *Plugin {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
return &Plugin{
Plugin: generic.NewPlugin(
handler,
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
return generic.NewPolicySource(
f.Admissionregistration().V1beta1().ValidatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1beta1().ValidatingAdmissionPolicyBindings().Informer(),
NewValidatingAdmissionPolicyAccessor,
NewValidatingAdmissionPolicyBindingAccessor,
compilePolicy,
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, NewMatcher(m))
},
),
}
}
// Validate makes an admission decision based on the request attributes.
func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
return a.Plugin.Dispatch(ctx, attr, o)
}
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
}
func compilePolicy(policy *Policy) Validator {
hasParam := false
if policy.Spec.ParamKind != nil {
hasParam = true
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
failurePolicy := convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policy.Spec.FailurePolicy)
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
filterCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
if err == nil {
filterCompiler.CompileAndStoreVariables(convertv1beta1Variables(policy.Spec.Variables), optionalVars, environment.StoredExpressions)
} else {
//!TODO: return a validator that always fails with internal error?
utilruntime.HandleError(err)
}
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
}
res := NewValidator(
filterCompiler.Compile(convertv1beta1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1beta1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1beta1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)
return res
}
func convertv1beta1FailurePolicyTypeTov1FailurePolicyType(policyType *v1beta1.FailurePolicyType) *v1.FailurePolicyType {
if policyType == nil {
return nil
}
var v1FailPolicy v1.FailurePolicyType
if *policyType == v1beta1.Fail {
v1FailPolicy = v1.Fail
} else if *policyType == v1beta1.Ignore {
v1FailPolicy = v1.Ignore
}
return &v1FailPolicy
}
func convertv1beta1Validations(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := ValidationCondition{
Expression: validation.Expression,
Message: validation.Message,
Reason: validation.Reason,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func convertv1beta1MessageExpressions(inputValidations []v1beta1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
if validation.MessageExpression != "" {
condition := MessageExpressionCondition{
MessageExpression: validation.MessageExpression,
}
celExpressionAccessor[i] = &condition
}
}
return celExpressionAccessor
}
func convertv1beta1AuditAnnotations(inputValidations []v1beta1.AuditAnnotation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := AuditAnnotationCondition{
Key: validation.Key,
ValueExpression: validation.ValueExpression,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func convertv1beta1Variables(variables []v1beta1.Variable) []cel.NamedExpressionAccessor {
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
for i, variable := range variables {
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
}
return namedExpressions
}

View File

@ -26,11 +26,35 @@ import (
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/cel/openapi/resolver" "k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/kube-openapi/pkg/validation/spec" "k8s.io/kube-openapi/pkg/validation/spec"
) )
var (
scheme *runtime.Scheme = func() *runtime.Scheme {
res := runtime.NewScheme()
if err := v1beta1.AddToScheme(res); err != nil {
panic(err)
}
if err := fake.AddToScheme(res); err != nil {
panic(err)
}
return res
}()
)
func must3[T any, I any](val T, _ I, err error) T {
if err != nil {
panic(err)
}
return val
}
func TestExtractTypeNames(t *testing.T) { func TestExtractTypeNames(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string