rebase: update K8s packages to v0.32.1

Update K8s packages in go.mod to v0.32.1

Signed-off-by: Praveen M <m.praveen@ibm.com>
This commit is contained in:
Praveen M
2025-01-16 09:41:46 +05:30
committed by mergify[bot]
parent 5aef21ea4e
commit 7eb99fc6c9
2442 changed files with 273386 additions and 47788 deletions

View File

@ -26,6 +26,7 @@ type PolicyAccessor interface {
GetNamespace() string
GetParamKind() *v1.ParamKind
GetMatchConstraints() *v1.MatchResources
GetFailurePolicy() *v1.FailurePolicyType
}
type BindingAccessor interface {

View File

@ -49,6 +49,9 @@ type Source[H Hook] interface {
// Dispatcher dispatches evaluates an admission request against the currently
// active hooks returned by the source.
type Dispatcher[H Hook] interface {
// Start the dispatcher. This method should be called only once at startup.
Start(ctx context.Context) error
// Dispatch a request to the policies. Dispatcher may choose not to
// call a hook, either because the rules of the hook does not match, or
// the namespaceSelector or the objectSelector of the hook does not

View File

@ -36,8 +36,9 @@ import (
)
// H is the Hook type generated by the source and consumed by the dispatcher.
// !TODO: Just pass in a Plugin[H] with accessors to all this information
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
// admissionResources is the list of resources related to CEL-based admission
// features.
@ -170,7 +171,7 @@ func (c *Plugin[H]) ValidateInitialization() error {
}
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
go func() {
@ -181,10 +182,15 @@ func (c *Plugin[H]) ValidateInitialization() error {
go func() {
err := c.source.Run(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
}
}()
err := c.dispatcher.Start(pluginContext)
if err != nil && !errors.Is(err, context.Canceled) {
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
}
c.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
})

View File

@ -36,7 +36,7 @@ import (
"k8s.io/client-go/tools/cache"
)
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
// in the context of a specific request. The params have already been resolved
// and any error in configuration or setting up the invocation is stored in
// the Error field.
@ -62,10 +62,6 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
// Params fetched by the binding to use to evaluate the policy
Param runtime.Object
// Error is set if there was an error with the policy or binding or its
// params, etc
Error error
}
// dispatcherDelegate is called during a request with a pre-filtered list
@ -76,7 +72,7 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
//
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) ([]PolicyError, *apierrors.StatusError)
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
newPolicyAccessor func(P) PolicyAccessor
@ -104,7 +100,10 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
// request. It then resolves all params and creates an Invocation for each
// matching policy-binding-param tuple. The delegate is then called with the
// list of tuples.
//
func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error {
return nil
}
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
// is expected to ignore the result of any policies whose match conditions dont pass.
// This may be possible to refactor so matchconditions are checked here instead.
@ -117,29 +116,33 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
objectInterfaces: o,
}
var policyErrors []PolicyError
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
var message error
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err)
} else {
message = fmt.Errorf("failed to configure binding: %w", err)
}
policyErrors = append(policyErrors, PolicyError{
Policy: definition,
Binding: binding,
Message: message,
})
}
for _, hook := range hooks {
policyAccessor := d.newPolicyAccessor(hook.Policy)
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
if err != nil {
// There was an error evaluating if this policy matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: err,
})
addConfigError(err, policyAccessor, nil)
continue
} else if !matches {
continue
} else if hook.ConfigurationError != nil {
// The policy matches but there is a configuration error with the
// policy itself
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Error: hook.ConfigurationError,
Resource: matchGVR,
Kind: matchGVK,
})
utilruntime.HandleError(hook.ConfigurationError)
addConfigError(hook.ConfigurationError, policyAccessor, nil)
continue
}
@ -148,19 +151,22 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
if err != nil {
// There was an error evaluating if this binding matches anything.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
} else if !matches {
continue
}
// here the binding matches.
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
// VersionedAttr result will be cached and reused later during parallel
// hook calls.
addConfigError(err, policyAccessor, nil)
continue
}
// Collect params for this binding
params, err := CollectParams(
policyAccessor.GetParamKind(),
@ -171,14 +177,7 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
)
if err != nil {
// There was an error collecting params for this binding.
utilruntime.HandleError(err)
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
Policy: hook.Policy,
Binding: binding,
Error: err,
Resource: matchGVR,
Kind: matchGVK,
})
addConfigError(err, policyAccessor, bindingAccessor)
continue
}
@ -194,23 +193,72 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
Evaluator: hook.Evaluator,
})
}
}
}
// VersionedAttr result will be cached and reused later during parallel
// hook calls
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
if err != nil {
return apierrors.NewInternalError(err)
}
if len(relevantHooks) > 0 {
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
if statusError != nil {
return statusError
}
policyErrors = append(policyErrors, extraPolicyErrors...)
}
var filteredErrors []PolicyError
for _, e := range policyErrors {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1.FailurePolicyType
if fp := e.Policy.GetFailurePolicy(); fp == nil {
policy = v1.Fail
} else {
policy = *fp
}
switch policy {
case v1.Ignore:
// TODO: add metrics for ignored error here
continue
case v1.Fail:
filteredErrors = append(filteredErrors, e)
default:
filteredErrors = append(filteredErrors, e)
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
if len(filteredErrors) > 0 {
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
// The forbiddenErr is always a StatusError.
var err *apierrors.StatusError
if !errors.As(forbiddenErr, &err) {
// Should never happen.
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
}
err.ErrStatus.Message = ""
for _, policyError := range filteredErrors {
message := policyError.Error()
// If this is the first denied decision, use its message and reason
// for the status error message.
if err.ErrStatus.Message == "" {
err.ErrStatus.Message = message
if policyError.Reason != "" {
err.ErrStatus.Reason = policyError.Reason
}
}
// Add the denied decision's message to the status error's details
err.ErrStatus.Details.Causes = append(
err.ErrStatus.Details.Causes,
metav1.StatusCause{Message: message})
}
return err
}
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
return nil
}
// Returns params to use to evaluate a policy-binding with given param
@ -352,3 +400,18 @@ func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionK
v.versionedAttrs[gvk] = versionedAttr
return versionedAttr, nil
}
type PolicyError struct {
Policy PolicyAccessor
Binding BindingAccessor
Message error
Reason metav1.StatusReason
}
func (c PolicyError) Error() string {
if c.Binding != nil {
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
}
return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error())
}

View File

@ -41,6 +41,13 @@ import (
"k8s.io/klog/v2"
)
// Interval for refreshing policies.
// TODO: Consider reducing this to a shorter duration or replacing this entirely
// with checks that detect when a policy change took effect.
const policyRefreshIntervalDefault = 1 * time.Second
var policyRefreshInterval = policyRefreshIntervalDefault
type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct {
ctx context.Context
policyInformer generic.Informer[P]
@ -122,6 +129,15 @@ func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator](
return res
}
// SetPolicyRefreshIntervalForTests allows the refresh interval to be overridden during tests.
// This should only be called from tests.
func SetPolicyRefreshIntervalForTests(interval time.Duration) func() {
policyRefreshInterval = interval
return func() {
policyRefreshInterval = policyRefreshIntervalDefault
}
}
func (s *policySource[P, B, E]) Run(ctx context.Context) error {
if s.ctx != nil {
return fmt.Errorf("policy source already running")
@ -178,7 +194,7 @@ func (s *policySource[P, B, E]) Run(ctx context.Context) error {
// and needs to be recompiled
go func() {
// Loop every 1 second until context is cancelled, refreshing policies
wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done())
wait.Until(s.refreshPolicies, policyRefreshInterval, ctx.Done())
}()
<-ctx.Done()

View File

@ -45,7 +45,6 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
)
// PolicyTestContext is everything you need to unit test a policy plugin
@ -196,18 +195,6 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator](
plugin.SetEnabled(true)
featureGate := featuregate.NewFeatureGate()
err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
//!TODO: move this to validating specific tests
features.ValidatingAdmissionPolicy: {
Default: true, PreRelease: featuregate.Beta}})
if err != nil {
return nil, nil, err
}
err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true})
if err != nil {
return nil, nil, err
}
testContext, testCancel := context.WithCancel(context.Background())
genericInitializer := initializer.New(
nativeClient,

View File

@ -0,0 +1,144 @@
/*
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 mutating
import (
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
)
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
return &mutatingAdmissionPolicyAccessor{
Policy: obj,
}
}
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
return &mutatingAdmissionPolicyBindingAccessor{
PolicyBinding: obj,
}
}
type mutatingAdmissionPolicyAccessor struct {
*Policy
}
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
pk := v.Spec.ParamKind
if pk == nil {
return nil
}
return &v1.ParamKind{
APIVersion: pk.APIVersion,
Kind: pk.Kind,
}
}
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
}
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return toV1FailurePolicy(v.Spec.FailurePolicy)
}
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if failurePolicy == nil {
return nil
}
fp := v1.FailurePolicyType(*failurePolicy)
return &fp
}
type mutatingAdmissionPolicyBindingAccessor struct {
*PolicyBinding
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
return v.Namespace
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
return v.Name
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
return types.NamespacedName{
Namespace: "",
Name: v.Spec.PolicyName,
}
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
}
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
if v.Spec.ParamRef == nil {
return nil
}
var nfa *v1.ParameterNotFoundActionType
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
nfa = new(v1.ParameterNotFoundActionType)
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
}
return &v1.ParamRef{
Name: v.Spec.ParamRef.Name,
Namespace: v.Spec.ParamRef.Namespace,
Selector: v.Spec.ParamRef.Selector,
ParameterNotFoundAction: nfa,
}
}
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
if mc == nil {
return nil
}
var res v1.MatchResources
res.NamespaceSelector = mc.NamespaceSelector
res.ObjectSelector = mc.ObjectSelector
for _, ex := range mc.ExcludeResourceRules {
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
for _, ex := range mc.ResourceRules {
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
ResourceNames: ex.ResourceNames,
RuleWithOperations: ex.RuleWithOperations,
})
}
if mc.MatchPolicy != nil {
mp := v1.MatchPolicyType(*mc.MatchPolicy)
res.MatchPolicy = &mp
}
return &res
}

View File

@ -0,0 +1,81 @@
/*
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 mutating
import (
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
// compilePolicy compiles the policy into a PolicyEvaluator
// any error is stored and delayed until invocation.
//
// Each individual mutation is compiled into MutationEvaluationFunc and
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
func compilePolicy(policy *Policy) PolicyEvaluator {
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
if err != nil {
return PolicyEvaluator{Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
}}
}
// Compile and store variables
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
// Compile matchers
var matcher matchconditions.Matcher = nil
matchConditions := policy.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
}
// Compiler patchers
var patchers []patch.Patcher
patchOptions := opts
patchOptions.HasPatchTypes = true
for _, m := range policy.Spec.Mutations {
switch m.PatchType {
case v1alpha1.PatchTypeJSONPatch:
if m.JSONPatch != nil {
accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
}
case v1alpha1.PatchTypeApplyConfiguration:
if m.ApplyConfiguration != nil {
accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
}
}
}
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
}

View File

@ -0,0 +1,295 @@
/*
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 mutating
import (
"context"
"errors"
"fmt"
"k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"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/policy/mutating/patch"
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
)
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
res := &dispatcher{
matcher: m,
authz: a,
typeConverterManager: tcm,
}
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
m,
res.dispatchInvocations,
)
return res
}
type dispatcher struct {
matcher *matching.Matcher
authz authorizer.Authorizer
typeConverterManager patch.TypeConverterManager
generic.Dispatcher[PolicyHook]
}
func (d *dispatcher) Start(ctx context.Context) error {
go d.typeConverterManager.Run(ctx)
return d.Dispatcher.Start(ctx)
}
func (d *dispatcher) dispatchInvocations(
ctx context.Context,
a admission.Attributes,
o admission.ObjectInterfaces,
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
) ([]generic.PolicyError, *k8serrors.StatusError) {
var lastVersionedAttr *admission.VersionedAttributes
reinvokeCtx := a.GetReinvocationContext()
var policyReinvokeCtx *policyReinvokeContext
if v := reinvokeCtx.Value(PluginName); v != nil {
policyReinvokeCtx = v.(*policyReinvokeContext)
} else {
policyReinvokeCtx = &policyReinvokeContext{}
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
}
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
// and we need to reinvoke all eligible policies.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
}
defer func() {
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
}()
var policyErrors []generic.PolicyError
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
policyErrors = append(policyErrors, generic.PolicyError{
Message: err,
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
Reason: reason,
})
}
// There is at least one invocation to invoke. Make sure we have a namespace
// object if the incoming object is not cluster scoped to pass into the evaluator.
var namespace *v1.Namespace
var err error
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = d.matcher.GetNamespace(namespaceName)
if err != nil {
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
}
}
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
// Should loop through invocations, handling possible error and invoking
// evaluator to apply patch, also should handle re-invocations
for _, invocation := range invocations {
if invocation.Evaluator.CompositionEnv != nil {
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
}
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
// This would be a bug. The compiler should always return exactly as
// many evaluators as there are mutations
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators)))
}
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
if err != nil {
// This should never happen, we pre-warm versoined attribute
// accessors before starting the dispatcher
return nil, k8serrors.NewInternalError(err)
}
if invocation.Evaluator.Matcher != nil {
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
if matchResults.Error != nil {
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
continue
}
// if preconditions are not met, then skip mutations
if !matchResults.Matches {
continue
}
}
invocationKey, invocationKeyErr := keyFor(invocation)
if invocationKeyErr != nil {
// This should never happen. It occurs if there is a programming
// error causing the Param not to be a valid object.
return nil, k8serrors.NewInternalError(invocationKeyErr)
}
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
continue
}
objectBeforeMutations := versionedAttr.VersionedObject
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
// in order.
for mutationIndex := range invocation.Policy.Spec.Mutations {
lastVersionedAttr = versionedAttr
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
continue
}
patcher := invocation.Evaluator.Mutators[mutationIndex]
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
if err != nil {
var statusError *k8serrors.StatusError
if errors.As(err, &statusError) {
return nil, statusError
}
addConfigError(err, invocation, metav1.StatusReasonInvalid)
continue
}
}
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
}
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
}
}
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
reinvokeCtx.SetShouldReinvoke()
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
}
}
return policyErrors, nil
}
func (d *dispatcher) dispatchOne(
ctx context.Context,
patcher patch.Patcher,
o admission.ObjectInterfaces,
versionedAttributes *admission.VersionedAttributes,
namespace *v1.Namespace,
resource schema.GroupVersionResource,
optionalVariables cel.OptionalVariableBindings,
) (err error) {
if patcher == nil {
// internal error. this should not happen
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
}
// Find type converter for the invoked Group-Version.
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
if typeConverter == nil {
// This can happen if the request is for a resource whose schema
// has not been registered with the type converter manager.
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
}
patchRequest := patch.Request{
MatchedResource: resource,
VersionedAttributes: versionedAttributes,
ObjectInterfaces: o,
OptionalVariables: optionalVariables,
Namespace: namespace,
TypeConverter: typeConverter,
}
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
if err != nil {
return err
}
switch versionedAttributes.VersionedObject.(type) {
case *unstructured.Unstructured:
// No conversion needed before defaulting for the patch object if the admitted object is unstructured.
default:
// Before defaulting, if the admitted object is a typed object, convert unstructured patch result back to a typed object.
newVersionedObject, err = o.GetObjectConvertor().ConvertToVersion(newVersionedObject, versionedAttributes.GetKind().GroupVersion())
if err != nil {
return err
}
}
o.GetObjectDefaulter().Default(newVersionedObject)
versionedAttributes.Dirty = true
versionedAttributes.VersionedObject = newVersionedObject
return nil
}
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
var paramUID types.NamespacedName
if invocation.Param != nil {
paramAccessor, err := meta.Accessor(invocation.Param)
if err != nil {
// This should never happen, as the param should have been validated
// before being passed to the plugin.
return key{}, err
}
paramUID = types.NamespacedName{
Name: paramAccessor.GetName(),
Namespace: paramAccessor.GetNamespace(),
}
}
return key{
PolicyUID: types.NamespacedName{
Name: invocation.Policy.GetName(),
Namespace: invocation.Policy.GetNamespace(),
},
BindingUID: types.NamespacedName{
Name: invocation.Binding.GetName(),
Namespace: invocation.Binding.GetNamespace(),
},
ParamUID: paramUID,
}, nil
}

View File

@ -0,0 +1,45 @@
/*
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 patch
import (
"context"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
type Patcher interface {
// Patch returns a copy of the object in the request, modified to change specified by the patch.
// The original object in the request MUST NOT be modified in-place.
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
}
// Request defines the arguments required by a patcher.
type Request struct {
MatchedResource schema.GroupVersionResource
VersionedAttributes *admission.VersionedAttributes
ObjectInterfaces admission.ObjectInterfaces
OptionalVariables cel.OptionalVariableBindings
Namespace *v1.Namespace
TypeConverter managedfields.TypeConverter
}

View File

@ -0,0 +1,192 @@
/*
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 patch
import (
"context"
gojson "encoding/json"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
"reflect"
"strconv"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
jsonpatch "gopkg.in/evanphx/json-patch.v4"
admissionv1 "k8s.io/api/admission/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
pointer "k8s.io/utils/ptr"
)
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
// that returns a JSON patch value.
type JSONPatchCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &JSONPatchCondition{}
func (v *JSONPatchCondition) GetExpression() string {
return v.Expression
}
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.ListType(jsonPatchType)}
}
var jsonPatchType = types.NewObjectType("JSONPatch")
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
return &jsonPatcher{patchEvaluator}
}
type jsonPatcher struct {
PatchEvaluator plugincel.MutatingEvaluator
}
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.PatchEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
if err != nil {
return nil, err
}
o := r.ObjectInterfaces
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
if errors.Is(err, jsonpatch.ErrTestFailed) {
// If a json patch fails a test operation, the patch must not be applied
return r.VersionedAttributes.VersionedObject, nil
}
return nil, fmt.Errorf("JSON Patch: %w", err)
}
var newVersionedObject runtime.Object
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
newVersionedObject = &unstructured.Unstructured{}
} else {
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
if err != nil {
return nil, apierrors.NewInternalError(err)
}
}
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
return nil, apierrors.NewInternalError(err)
}
return newVersionedObject, nil
}
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
var err error
var eval plugincel.EvaluationResult
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
if err != nil {
return nil, -1, err
}
if eval.Error != nil {
return nil, -1, eval.Error
}
refVal := eval.EvalResult
// the return type can be any valid CEL value.
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
// possible in Kubernetes CEL because maps and lists may not have mixed types.
iter, ok := refVal.(traits.Lister)
if !ok {
// Should never happen since compiler checks return type.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
}
result := jsonpatch.Patch{}
for it := iter.Iterator(); it.HasNext() == types.True; {
v := it.Next()
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
if err != nil {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
}
op, ok := patchObj.(*mutation.JSONPatchVal)
if !ok {
// Should never happen since return type is checked by compiler.
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
}
// Construct a JSON Patch from the evaluated CEL expression
resultOp := jsonpatch.Operation{}
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
if len(op.From) > 0 {
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
}
if op.Val != nil {
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err := objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, -1, fmt.Errorf("type mismatch: %w", err)
}
}
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
// JSON Patch if first converted to pb.Value.
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
b, err := gojson.Marshal(v)
if err != nil {
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
}
resultOp["value"] = pointer.To[gojson.RawMessage](b)
}
result = append(result, resultOp)
}
return result, remainingBudget, nil
}

View File

@ -0,0 +1,217 @@
/*
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 patch
import (
"context"
"errors"
"fmt"
celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/managedfields"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
// that returns an apply configuration
type ApplyConfigurationCondition struct {
Expression string
}
var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{}
func (v *ApplyConfigurationCondition) GetExpression() string {
return v.Expression
}
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{applyConfigObjectType}
}
var applyConfigObjectType = celtypes.NewObjectType("Object")
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
}
type applyConfigPatcher struct {
expressionEvaluator plugincel.MutatingEvaluator
}
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
admissionRequest := plugincel.CreateAdmissionRequest(
r.VersionedAttributes.Attributes,
metav1.GroupVersionResource(r.MatchedResource),
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
compileErrors := e.expressionEvaluator.CompilationErrors()
if len(compileErrors) > 0 {
return nil, errors.Join(compileErrors...)
}
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
if err != nil {
return nil, err
}
if eval.Error != nil {
return nil, eval.Error
}
v := eval.EvalResult
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
objVal, ok := v.(*dynamic.ObjectVal)
if !ok {
// Should not happen since the compiler type checks the return type.
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
}
// TODO: Object initializers are insufficiently type checked.
// In the interim, we use this sanity check to detect type mismatches
// between field names and Object initializers. For example,
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
// Before beta, attaching full type information both to Object initializers and
// the "object" and "oldObject" variables is needed. This will allow CEL to
// perform comprehensive runtime type checking.
err = objVal.CheckTypeNamesMatchFieldPathNames()
if err != nil {
return nil, fmt.Errorf("type mismatch: %w", err)
}
value, ok := objVal.Value().(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid return type: %T", v)
}
patchObject := unstructured.Unstructured{Object: value}
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
if err != nil {
return nil, fmt.Errorf("error applying patch: %w", err)
}
return patched, nil
}
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
// with the patch applied.
func ApplyStructuredMergeDiff(
typeConverter managedfields.TypeConverter,
originalObject runtime.Object,
patch *unstructured.Unstructured,
) (runtime.Object, error) {
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
} else if typeConverter == nil {
return nil, fmt.Errorf("type converter must not be nil")
}
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
if err != nil {
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
}
err = validatePatch(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
}
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
if err != nil {
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
}
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to merge patch: %w", err)
}
// Our mutating admission policy sets the fields but does not track ownership.
// Newly introduced fields in the patch won't be tracked by a field manager
// (so if the original object is updated again but the mutating policy is
// not active, the fields will be dropped).
//
// This necessarily means that changes to an object by a mutating policy
// are only preserved if the policy was active at the time of the change.
// (If the policy is not active, the changes may be dropped.)
newObj, err := typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
}
return newObj, nil
}
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
// an error if any are found.
// This prevents accidental removal of fields that can occur when the user intends to modify some
// fields in an atomic type, not realizing that all fields not explicitly set in the new value
// of the atomic will be removed.
func validatePatch(v *typed.TypedValue) error {
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
if len(atomics) > 0 {
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
}
return nil
}
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
if v.IsMap() && a.Map != nil {
if a.Map.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
v.AsMap().Iterate(func(key string, val value.Value) bool {
pe := fieldpath.PathElement{FieldName: &key}
if sf, ok := a.Map.FindField(key); ok {
tr = sf.Type
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
}
return true
})
}
if v.IsList() && a.List != nil {
if a.List.ElementRelationship == schema.Atomic {
atomics = append(atomics, pathString(path))
}
list := v.AsList()
for i := 0; i < list.Length(); i++ {
pe := fieldpath.PathElement{Index: &i}
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
}
}
}
return atomics
}
func pathString(path []fieldpath.PathElement) string {
sb := strings.Builder{}
for _, p := range path {
sb.WriteString(p.String())
}
return sb.String()
}

View File

@ -0,0 +1,187 @@
/*
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 patch
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/openapi"
"k8s.io/kube-openapi/pkg/spec3"
)
type TypeConverterManager interface {
// GetTypeConverter returns a type converter for the given GVK
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
Run(ctx context.Context)
}
func NewTypeConverterManager(
staticTypeConverter managedfields.TypeConverter,
openapiClient openapi.Client,
) TypeConverterManager {
return &typeConverterManager{
staticTypeConverter: staticTypeConverter,
openapiClient: openapiClient,
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
}
}
type typeConverterCacheEntry struct {
typeConverter managedfields.TypeConverter
entry openapi.GroupVersion
}
// typeConverterManager helps us make sure we have an up to date schema and
// type converter for our openapi models. It should be connfigured to use a
// static type converter for natively typed schemas, and fetches the schema
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
type typeConverterManager struct {
// schemaCache is used to cache the schema for a given GVK
staticTypeConverter managedfields.TypeConverter
// discoveryClient is used to fetch the schema for a given GVK
openapiClient openapi.Client
lock sync.RWMutex
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
}
func (t *typeConverterManager) Run(ctx context.Context) {
// Loop every 5s refershing the OpenAPI schema list to know which
// schemas have been invalidated. This should use e-tags under the hood
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
paths, err := t.openapiClient.Paths()
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
return false, nil
}
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
// value changes every time the schema is updated. So we poll /openapi/v3
// to get the "version number" for each schema, and invalidate our cache
// if the version number has changed since we pulled it.
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
for path, entry := range paths {
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
continue
}
path = strings.TrimPrefix(path, "apis/")
path = strings.TrimPrefix(path, "api/")
gv, err := schema.ParseGroupVersion(path)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
return false, nil
}
parsedPaths[gv] = entry
}
t.lock.Lock()
defer t.lock.Unlock()
t.lastFetchedPaths = parsedPaths
return false, nil
})
}
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
// Check to see if the static type converter handles this GVK
if t.staticTypeConverter != nil {
//!TODO: Add ability to check existence to type converter
// working around for now but seeing if getting a typed version of an
// empty object returns error
stub := &unstructured.Unstructured{}
stub.SetGroupVersionKind(gvk)
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
return t.staticTypeConverter
}
}
gv := gvk.GroupVersion()
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
t.lock.RLock()
defer t.lock.RUnlock()
// If schema is not supported by static type converter, ask discovery
// for the schema
entry, ok := t.lastFetchedPaths[gv]
if !ok {
// If we can't get the schema, we can't do anything
return nil, nil, fmt.Errorf("no schema for %v", gvk)
}
// If the entry schema has not changed, used the same type converter
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
// If we have a type converter for this GVK, return it
return existing.typeConverter, existing.entry, nil
}
return nil, entry, nil
}()
if err != nil {
utilruntime.HandleError(err)
return nil
} else if existing != nil {
return existing
}
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
return nil
}
var sch spec3.OpenAPI
if err := json.Unmarshal(schBytes, &sch); err != nil {
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
return nil
}
// The schema has changed, or there is no entry for it, generate
// a new type converter for this GV
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
return nil
}
t.lock.Lock()
defer t.lock.Unlock()
t.typeConverterMap[gv] = typeConverterCacheEntry{
typeConverter: tc,
entry: entry,
}
return tc
}

View File

@ -0,0 +1,151 @@
/*
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 mutating
import (
"context"
celgo "github.com/google/cel-go/cel"
"io"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission"
"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/policy/mutating/patch"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/authorization/authorizer"
"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 = "MutatingAdmissionPolicy"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
return NewPlugin(configFile), nil
})
}
type Policy = v1alpha1.MutatingAdmissionPolicy
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
type PolicyMutation = v1alpha1.Mutation
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
type Mutator struct {
}
type MutationEvaluationFunc func(
ctx context.Context,
matchedResource schema.GroupVersionResource,
versionedAttr *admission.VersionedAttributes,
o admission.ObjectInterfaces,
versionedParams runtime.Object,
namespace *corev1.Namespace,
typeConverter managedfields.TypeConverter,
runtimeCELCostBudget int64,
authorizer authorizer.Authorizer,
) (runtime.Object, error)
type PolicyEvaluator struct {
Matcher matchconditions.Matcher
Mutators []patch.Patcher
CompositionEnv *cel.CompositionEnv
Error error
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Plugin[PolicyHook]
}
var _ admission.Interface = &Plugin{}
var _ admission.MutationInterface = &Plugin{}
// NewPlugin returns a generic admission webhook plugin.
func NewPlugin(_ io.Reader) *Plugin {
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
res := &Plugin{}
res.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().V1alpha1().MutatingAdmissionPolicies().Informer(),
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
NewMutatingAdmissionPolicyAccessor,
NewMutatingAdmissionPolicyBindingAccessor,
compilePolicy,
//!TODO: Create a way to share param informers between
// mutating/validating plugins
f,
dynamicClient,
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
},
)
return res
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(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.MutatingAdmissionPolicy))
}
// Variable is a named expression for composition.
type Variable struct {
Name string
Expression string
}
func (v *Variable) GetExpression() string {
return v.Expression
}
func (v *Variable) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.AnyType, celgo.DynType}
}
func (v *Variable) GetName() string {
return v.Name
}
func convertv1alpha1Variables(variables []v1alpha1.Variable) []cel.NamedExpressionAccessor {
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
for i, variable := range variables {
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
}
return namedExpressions
}

View File

@ -0,0 +1,76 @@
/*
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 mutating
import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
)
type key struct {
PolicyUID types.NamespacedName
BindingUID types.NamespacedName
ParamUID types.NamespacedName
MutationIndex int
}
type policyReinvokeContext struct {
// lastPolicyOutput holds the result of the last Policy admission plugin call
lastPolicyOutput runtime.Object
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
// should be reinvoked if a later mutation occurs
previouslyInvokedReinvocablePolicies sets.Set[key]
// reinvokePolicies holds the set of Policies that should be reinvoked
reinvokePolicies sets.Set[key]
}
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
return rc.reinvokePolicies.Has(policy)
}
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
}
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
if object == nil {
rc.lastPolicyOutput = nil
return
}
rc.lastPolicyOutput = object.DeepCopyObject()
}
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
if rc.previouslyInvokedReinvocablePolicies == nil {
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
}
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
if rc.reinvokePolicies == nil {
rc.reinvokePolicies = sets.New[key]()
}
for s := range rc.previouslyInvokedReinvocablePolicies {
rc.reinvokePolicies.Insert(s)
}
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
}
}

View File

@ -54,6 +54,10 @@ func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResou
return v.Spec.MatchConstraints
}
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
return v.Spec.FailurePolicy
}
type validatingAdmissionPolicyBindingAccessor struct {
*v1.ValidatingAdmissionPolicyBinding
}

View File

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

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiserver/pkg/admission"
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
celconfig "k8s.io/apiserver/pkg/apis/cel"
@ -63,6 +64,10 @@ type policyDecisionWithMetadata struct {
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
}
func (c *dispatcher) Start(ctx context.Context) error {
return nil
}
// Dispatch implements generic.Dispatcher.
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
@ -109,7 +114,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm
}
}
authz := newCachingAuthorizer(c.authz)
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
for _, hook := range hooks {
// versionedAttributes will be set to non-nil inside of the loop, but

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
package metrics
import (
"errors"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
package metrics
import (
"context"

View File

@ -36,7 +36,6 @@ import (
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
)
const (
@ -93,13 +92,12 @@ type Plugin struct {
var _ admission.Interface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{}
var _ initializer.WantsFeatures = &Plugin{}
var _ initializer.WantsExcludedAdmissionResources = &Plugin{}
func NewPlugin(_ io.Reader) *Plugin {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
return &Plugin{
p := &Plugin{
Plugin: generic.NewPlugin(
handler,
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
@ -114,11 +112,13 @@ func NewPlugin(_ io.Reader) *Plugin {
restMapper,
)
},
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
return NewDispatcher(a, generic.NewPolicyMatcher(m))
},
),
}
p.SetEnabled(true)
return p
}
// Validate makes an admission decision based on the request attributes.
@ -126,10 +126,6 @@ func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admi
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 {
@ -155,13 +151,13 @@ func compilePolicy(policy *Policy) Validator {
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
}
res := NewValidator(
filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
)

View File

@ -41,13 +41,13 @@ import (
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
validationFilter cel.ConditionEvaluator
auditAnnotationFilter cel.ConditionEvaluator
messageFilter cel.ConditionEvaluator
failPolicy *v1.FailurePolicyType
}
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
@ -122,6 +122,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
decision.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
@ -146,6 +147,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
decision.Evaluation = EvalDeny
if validation.Reason == nil {
decision.Reason = metav1.StatusReasonInvalid
} else {
@ -210,6 +212,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
continue
}
var auditAnnotationResult = &auditAnnotationResults[i]
auditAnnotationResult.Elapsed = evalResult.Elapsed
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {