Add mutation support into CompositedCompiler and reorganize for clarity

Kubernetes-commit: 081353bf8ad963d43c5da6714a24f62cfe0b8401
This commit is contained in:
Joe Betz 2024-10-25 14:37:17 -04:00 committed by Kubernetes Publisher
parent 9ead80d1bb
commit 0e6467b270
14 changed files with 734 additions and 534 deletions

View File

@ -0,0 +1,190 @@
/*
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 cel
import (
"context"
"fmt"
"github.com/google/cel-go/interpreter"
"math"
"time"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
// newActivation creates an activation for CEL admission plugins from the given request, admission chain and
// variable binding information.
func newActivation(compositionCtx CompositionContext, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace) (*evaluationActivation, error) {
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, fmt.Errorf("failed to prepare params variable for evaluation: %w", err)
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy and MutatingAdmissionPolicy.
if compositionCtx != nil {
va.variables = compositionCtx.Variables(va)
}
return va, nil
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Evaluate runs a compiled CEL admission plugin expression using the provided activation and CEL
// runtime cost budget.
func (a *evaluationActivation) Evaluate(ctx context.Context, compositionCtx CompositionContext, compilationResult CompilationResult, remainingBudget int64) (EvaluationResult, int64, error) {
var evaluation = EvaluationResult{}
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
return evaluation, remainingBudget, nil
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
return evaluation, remainingBudget, nil
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: "unexpected internal error compiling expression",
}
return evaluation, remainingBudget, nil
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, a)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return evaluation, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
return evaluation, remainingBudget, nil
}

View File

@ -24,8 +24,10 @@ import (
"k8s.io/apimachinery/pkg/util/version"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/mutation"
)
const (
@ -186,7 +188,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType || cel.AnyType == returnType {
if ast.OutputType().IsExactType(returnType) || cel.AnyType.IsExactType(returnType) {
found = true
break
}
@ -194,9 +196,9 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType().String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
reason = fmt.Sprintf("must evaluate to one of %v but got %v", returnTypes, ast.OutputType().String())
}
return resultError(reason, apiservercel.ErrorTypeInvalid, nil)
@ -226,46 +228,78 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
envs := make(variableDeclEnvs, 8) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
var err error
for _, strictCost := range []bool{false, true} {
var envOpts []cel.EnvOption
if hasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if hasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
panic(err)
}
if strictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
}
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended
}
// We only need this ObjectTypes where strict cost is true
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true}
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
if err != nil {
panic(err)
}
}
}
return envs
}
func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.DeclType, requestType *apiservercel.DeclType, opts OptionalVariableDeclarations) (*environment.EnvSet, error) {
var envOpts []cel.EnvOption
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
if opts.HasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if opts.HasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},
)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
if opts.StrictCost {
extended, err = extended.Extend(environment.StrictCostOpt)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
if opts.HasPatchTypes {
extended, err = extended.Extend(hasPatchTypes)
if err != nil {
return nil, fmt.Errorf("environment misconfigured: %w", err)
}
}
return extended, nil
}
var hasPatchTypes = environment.VersionedOptions{
// Feature epoch was actually 1.32, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: []cel.EnvOption{
common.ResolverEnvOption(&mutation.DynamicTypeResolver{}),
library.JSONPatch(), // for jsonPatch.escape() function
},
}

View File

@ -36,15 +36,27 @@ import (
const VariablesTypeName = "kubernetes.variables"
// CompositedCompiler compiles expressions with variable composition.
type CompositedCompiler struct {
Compiler
FilterCompiler
ConditionCompiler
MutatingCompiler
CompositionEnv *CompositionEnv
}
type CompositedFilter struct {
Filter
// CompositedConditionEvaluator provides evaluation of a condition expression with variable composition.
// The expressions must return a boolean.
type CompositedConditionEvaluator struct {
ConditionEvaluator
compositionEnv *CompositionEnv
}
// CompositedEvaluator provides evaluation of a single expression with variable composition.
// The types that may returned by the expression is determined at compilation time.
type CompositedEvaluator struct {
MutatingEvaluator
compositionEnv *CompositionEnv
}
@ -64,11 +76,13 @@ func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompi
CompiledVariables: map[string]CompilationResult{},
}
compiler := NewCompiler(context.EnvSet)
filterCompiler := NewFilterCompiler(context.EnvSet)
conditionCompiler := &conditionCompiler{compiler}
mutation := &mutatingCompiler{compiler}
return &CompositedCompiler{
Compiler: compiler,
FilterCompiler: filterCompiler,
CompositionEnv: context,
Compiler: compiler,
ConditionCompiler: conditionCompiler,
MutatingCompiler: mutation,
CompositionEnv: context,
}
}
@ -85,11 +99,20 @@ func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAcc
return result
}
func (c *CompositedCompiler) Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter {
filter := c.FilterCompiler.Compile(expressions, optionalDecls, envType)
return &CompositedFilter{
Filter: filter,
compositionEnv: c.CompositionEnv,
func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator {
condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType)
return &CompositedConditionEvaluator{
ConditionEvaluator: condition,
compositionEnv: c.CompositionEnv,
}
}
// CompileEvaluator compiles an mutatingEvaluator for the given expression, options and environment.
func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator {
mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType)
return &CompositedEvaluator{
MutatingEvaluator: mutation,
compositionEnv: c.CompositionEnv,
}
}
@ -160,9 +183,9 @@ func (c *compositionContext) Variables(activation any) ref.Val {
return lazyMap
}
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
ctx = f.compositionEnv.CreateContext(ctx)
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
}
func (c *compositionContext) reportCost(cost int64) {

View File

@ -223,8 +223,8 @@ func TestCompositedPolicies(t *testing.T) {
t.Fatal(err)
}
compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
validations := []ExpressionAccessor{&condition{Expression: tc.expression}}
f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
validations := []ExpressionAccessor{&testCondition{Expression: tc.expression}}
f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
if err != nil {
t.Fatal(err)

View File

@ -0,0 +1,216 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"reflect"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// conditionCompiler implement the interface ConditionCompiler.
type conditionCompiler struct {
compiler Compiler
}
func NewConditionCompiler(env *environment.EnvSet) ConditionCompiler {
return &conditionCompiler{compiler: NewCompiler(env)}
}
// CompileCondition compiles the cel expressions defined in the ExpressionAccessors into a ConditionEvaluator
func (c *conditionCompiler) CompileCondition(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) ConditionEvaluator {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewCondition(compilationResults)
}
// condition implements the ConditionEvaluator interface
type condition struct {
compilationResults []CompilationResult
}
func NewCondition(compilationResults []CompilationResult) ConditionEvaluator {
return &condition{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (c *condition) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(c.compilationResults))
var err error
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return nil, -1, err
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range c.compilationResults {
evaluations[i], remainingBudget, err = activation.Evaluate(ctx, compositionCtx, compilationResult, remainingBudget)
if err != nil {
return nil, -1, err
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (c *condition) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range c.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@ -28,8 +28,6 @@ import (
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
pointer "k8s.io/utils/ptr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -48,17 +46,18 @@ import (
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
pointer "k8s.io/utils/ptr"
)
type condition struct {
type testCondition struct {
Expression string
}
func (c *condition) GetExpression() string {
return c.Expression
func (tc *testCondition) GetExpression() string {
return tc.Expression
}
func (v *condition) ReturnTypes() []*celgo.Type {
func (tc *testCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
@ -71,10 +70,10 @@ func TestCompile(t *testing.T) {
{
name: "invalid syntax",
validation: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "1 < 'asdf'",
},
&condition{
&testCondition{
Expression: "1 < 2",
},
},
@ -85,13 +84,13 @@ func TestCompile(t *testing.T) {
{
name: "valid syntax",
validation: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "1 < 2",
},
&condition{
&testCondition{
Expression: "object.spec.string.matches('[0-9]+')",
},
&condition{
&testCondition{
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
},
},
@ -100,13 +99,13 @@ func TestCompile(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
if e == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validation
CompilationResults := e.(*filter).compilationResults
CompilationResults := e.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
meets := make([]bool, len(validations))
@ -131,7 +130,7 @@ func TestCompile(t *testing.T) {
}
}
func TestFilter(t *testing.T) {
func TestCondition(t *testing.T) {
simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"})
if err != nil {
panic(err)
@ -205,7 +204,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for object",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
},
@ -220,7 +219,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for metadata",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.metadata.name == 'endpoints1'",
},
},
@ -235,10 +234,10 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for oldObject",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject == null",
},
&condition{
&testCondition{
Expression: "object != null",
},
},
@ -256,7 +255,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for request",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "request.operation == 'CREATE'",
},
},
@ -271,7 +270,7 @@ func TestFilter(t *testing.T) {
{
name: "valid syntax for configMap",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "request.namespace != params.data.fakeString",
},
},
@ -287,7 +286,7 @@ func TestFilter(t *testing.T) {
{
name: "test failure",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -310,10 +309,10 @@ func TestFilter(t *testing.T) {
{
name: "test failure with multiple validations",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -332,10 +331,10 @@ func TestFilter(t *testing.T) {
{
name: "test failure policy with multiple failed validations",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -354,10 +353,10 @@ func TestFilter(t *testing.T) {
{
name: "test Object null in delete",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object == null",
},
},
@ -376,7 +375,7 @@ func TestFilter(t *testing.T) {
{
name: "test runtime error",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject.x == 100",
},
},
@ -392,7 +391,7 @@ func TestFilter(t *testing.T) {
{
name: "test against crd param",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() < params.spec.testSize",
},
},
@ -408,10 +407,10 @@ func TestFilter(t *testing.T) {
{
name: "test compile failure",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "fail to compile test",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > params.spec.testSize",
},
},
@ -430,7 +429,7 @@ func TestFilter(t *testing.T) {
{
name: "test pod",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.spec.nodeName == 'testnode'",
},
},
@ -446,7 +445,7 @@ func TestFilter(t *testing.T) {
{
name: "test deny paramKind without paramRef",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "params != null",
},
},
@ -461,7 +460,7 @@ func TestFilter(t *testing.T) {
{
name: "test allow paramKind without paramRef",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "params == null",
},
},
@ -477,10 +476,10 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
},
},
@ -503,7 +502,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer error using fieldSelector with 1.30 compatibility",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@ -535,7 +534,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with all fields",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@ -567,7 +566,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with parse failures",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@ -595,7 +594,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow resource check with all fields, without gate",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
},
@ -621,7 +620,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer not allowed resource check one incorrect field",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
},
@ -646,7 +645,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer reason",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'",
},
},
@ -661,13 +660,13 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer error",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').error() == 'fake authz error'",
},
&condition{
&testCondition{
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -688,7 +687,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer allow path check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.path('/healthz').check('get').allowed()",
},
},
@ -706,7 +705,7 @@ func TestFilter(t *testing.T) {
{
name: "test authorizer decision is denied path check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.path('/healthz').check('get').allowed() == false",
},
},
@ -721,7 +720,7 @@ func TestFilter(t *testing.T) {
{
name: "test request resource authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
},
},
@ -745,7 +744,7 @@ func TestFilter(t *testing.T) {
{
name: "test subresource request resource authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
},
},
@ -769,7 +768,7 @@ func TestFilter(t *testing.T) {
{
name: "test serviceAccount authorizer allow check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()",
},
},
@ -796,7 +795,7 @@ func TestFilter(t *testing.T) {
{
name: "test perCallLimit exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "object.subsets.size() < params.spec.testSize",
},
},
@ -813,28 +812,28 @@ func TestFilter(t *testing.T) {
{
name: "test namespaceObject",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "namespaceObject.metadata.name == 'test'",
},
&condition{
&testCondition{
Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'",
},
&condition{
&testCondition{
Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'",
},
&condition{
&testCondition{
Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'",
},
&condition{
&testCondition{
Expression: "namespaceObject.status.phase == 'Active'",
},
&condition{
&testCondition{
Expression: "size(namespaceObject.metadata.managedFields) == 1",
},
&condition{
&testCondition{
Expression: "size(namespaceObject.metadata.ownerReferences) == 1",
},
&condition{
&testCondition{
Expression: "'env' in namespaceObject.metadata.annotations",
},
},
@ -891,14 +890,14 @@ func TestFilter(t *testing.T) {
if err != nil {
t.Fatal(err)
}
c := NewFilterCompiler(env)
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions)
c := NewConditionCompiler(env)
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validations
CompilationResults := f.(*filter).compilationResults
CompilationResults := f.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
@ -960,10 +959,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "expression exceed RuntimeCELCostBudget at fist expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
},
@ -975,10 +974,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "expression exceed RuntimeCELCostBudget at last expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -991,10 +990,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge is not exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -1008,10 +1007,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge exactly covers",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
},
@ -1025,13 +1024,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "test RuntimeCELCostBudge exactly covers then constant",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "oldObject != null",
},
&condition{
&testCondition{
Expression: "object.subsets.size() > 2",
},
&condition{
&testCondition{
Expression: "true", // zero cost
},
},
@ -1045,7 +1044,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: authz check",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -1059,7 +1058,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: isSorted()",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "[1,2,3,4].isSorted()",
},
},
@ -1072,7 +1071,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: url",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
},
},
@ -1085,7 +1084,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: split",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.split(' ')) > 0",
},
},
@ -1098,7 +1097,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: join",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
},
},
@ -1111,7 +1110,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: find",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.find('123')) > 0",
},
},
@ -1124,7 +1123,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "Extended library cost: quantity",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
},
},
@ -1137,10 +1136,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at fist expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "has(object.subsets)",
},
},
@ -1154,10 +1153,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -1171,10 +1170,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -1190,10 +1189,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -1209,7 +1208,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()",
},
},
@ -1224,7 +1223,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "[1,2,3,4].isSorted()",
},
},
@ -1238,7 +1237,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
},
},
@ -1252,7 +1251,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.split(' ')) > 0",
},
},
@ -1266,7 +1265,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
},
},
@ -1280,7 +1279,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "size('abc 123 def 123'.find('123')) > 0",
},
},
@ -1294,7 +1293,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
{
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity",
validations: []ExpressionAccessor{
&condition{
&testCondition{
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
},
},
@ -1309,13 +1308,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validations
CompilationResults := f.(*filter).compilationResults
CompilationResults := f.(*condition).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
@ -1476,7 +1475,7 @@ func TestCompilationErrors(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
e := filter{
e := condition{
compilationResults: tc.results,
}
compilationErrors := e.CompilationErrors()

View File

@ -1,361 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"context"
"fmt"
"math"
"reflect"
"time"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
compiler Compiler
}
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
return &filterCompiler{compiler: NewCompiler(env)}
}
type evaluationActivation struct {
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
case VariableVarName: // variables always present
return a.variables, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *evaluationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewFilter(compilationResults)
}
// filter implements the Filter interface
type filter struct {
compilationResults []CompilationResult
}
func NewFilter(compilationResults []CompilationResult) Filter {
return &filter{
compilationResults,
}
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(f.compilationResults))
var err error
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, -1, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, -1, err
}
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
if inputs.VersionedParams != nil {
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
if err != nil {
return nil, -1, err
}
}
if inputs.Authorizer != nil {
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, -1, err
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, -1, err
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
// composition is an optional feature that only applies for ValidatingAdmissionPolicy.
// check if the context allows composition
var compositionCtx CompositionContext
var ok bool
if compositionCtx, ok = ctx.(CompositionContext); ok {
va.variables = compositionCtx.Variables(va)
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range f.compilationResults {
var evaluation = &evaluations[i]
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
continue
}
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
if compilationResult.Error != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
Cause: compilationResult.Error,
}
continue
}
if compilationResult.Program == nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("unexpected internal error compiling expression"),
}
continue
}
t1 := time.Now()
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va)
// budget may be spent due to lazy evaluation of composited variables
if compositionCtx != nil {
compositionCost := compositionCtx.GetAndResetCost()
if compositionCost > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= compositionCost
}
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInternal,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
}
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
Cause: cel.ErrOutOfBudget,
}
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return nil, -1, &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
Cause: cel.ErrOutOfBudget,
}
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = &cel.Error{
Type: cel.ErrorTypeInvalid,
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
}
} else {
evaluation.EvalResult = evalResult
}
}
return evaluations, remainingBudget, nil
}
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
// Attempting to use same logic as webhook for constructing resource
// GVK, GVR, subresource
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
gvk := equivalentKind
gvr := equivalentGVR
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the evaluator
func (e *filter) CompilationErrors() []error {
compilationErrors := []error{}
for _, result := range e.compilationResults {
if result.Error != nil {
compilationErrors = append(compilationErrors, result.Error)
}
}
return compilationErrors
}

View File

@ -63,12 +63,15 @@ type OptionalVariableDeclarations struct {
HasAuthorizer bool
// StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries.
StrictCost bool
// HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used
// to initialize the typed objects in CEL required to create patches.
HasPatchTypes bool
}
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type FilterCompiler interface {
// Compile is used for the cel expression compilation
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
// ConditionCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type ConditionCompiler interface {
// CompileCondition is used for the cel expression compilation
CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.
@ -82,16 +85,38 @@ type OptionalVariableBindings struct {
Authorizer authorizer.Authorizer
}
// Filter contains a function to evaluate compiled CEL-typed values
// ConditionEvaluator contains the result of compiling a CEL expression
// that evaluates to a condition. This is used both for validation and pre-conditions.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type Filter interface {
type ConditionEvaluator interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the filter should return the remaining budget.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}
// MutatingCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type MutatingCompiler interface {
// CompileMutatingEvaluator is used for the cel expression compilation
CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator
}
// MutatingEvaluator contains the result of compiling a CEL expression
// that evaluates to a mutation.
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type MutatingEvaluator interface {
// ForInput converts compiled CEL-typed values into a CEL-typed value representing a mutation.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the condition should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
CompilationErrors() []error
}

View File

@ -0,0 +1,73 @@
/*
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 cel
import (
"context"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel/environment"
)
// mutatingCompiler provides a MutatingCompiler implementation.
type mutatingCompiler struct {
compiler Compiler
}
// CompileMutatingEvaluator compiles a CEL expression for admission plugins and returns an MutatingEvaluator for executing the
// compiled CEL expression.
func (p *mutatingCompiler) CompileMutatingEvaluator(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) MutatingEvaluator {
compilationResult := p.compiler.CompileCELExpression(expressionAccessor, options, mode)
return NewMutatingEvaluator(compilationResult)
}
type mutatingEvaluator struct {
compilationResult CompilationResult
}
func NewMutatingEvaluator(compilationResult CompilationResult) MutatingEvaluator {
return &mutatingEvaluator{compilationResult}
}
// ForInput evaluates the compiled CEL expression and returns an evaluation result
// errors per evaluation are returned in the evaluation result
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (p *mutatingEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) {
// if this activation supports composition, we will need the compositionCtx. It may be nil.
compositionCtx, _ := ctx.(CompositionContext)
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
if err != nil {
return EvaluationResult{}, -1, err
}
evaluation, remainingBudget, err := activation.Evaluate(ctx, compositionCtx, p.compilationResult, runtimeCELCostBudget)
if err != nil {
return evaluation, -1, err
}
return evaluation, remainingBudget, nil
}
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
func (p *mutatingEvaluator) CompilationErrors() (compilationErrors []error) {
if p.compilationResult.Error != nil {
return []error{p.compilationResult.Error}
}
return nil
}

View File

@ -50,7 +50,7 @@ type WebhookAccessor interface {
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
// GetCompiledMatcher gets the compiled matcher object
GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher
GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
// configuration and does not provide a globally unique identity, if a unique identity is
@ -132,7 +132,7 @@ func (m *mutatingWebhookAccessor) GetType() string {
return "admit"
}
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
m.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
for i, matchCondition := range m.MutatingWebhook.MatchConditions {
@ -145,7 +145,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,
@ -265,7 +265,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
return v.client, v.clientErr
}
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
v.compileMatcher.Do(func() {
expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions))
for i, matchCondition := range v.ValidatingWebhook.MatchConditions {
@ -278,7 +278,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
strictCost = true
}
v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
expressions,
cel.OptionalVariableDeclarations{
HasParams: false,

View File

@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -56,7 +57,7 @@ type Webhook struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher
filterCompiler cel.FilterCompiler
filterCompiler cel.ConditionCompiler
authorizer authorizer.Authorizer
}
@ -101,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm),
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
}, nil
}

View File

@ -76,7 +76,7 @@ type fakeWebhookAccessor struct {
matchResult bool
}
func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
return &fakeMatcher{
throwError: f.throwError,
matchResult: f.matchResult,

View File

@ -54,14 +54,14 @@ var _ Matcher = &matcher{}
// matcher evaluates compiled cel expressions and determines if they match the given request or not
type matcher struct {
filter celplugin.Filter
filter celplugin.ConditionEvaluator
failPolicy v1.FailurePolicyType
matcherType string
matcherKind string
objectName string
}
func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
func NewMatcher(filter celplugin.ConditionEvaluator, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
var f v1.FailurePolicyType
if failPolicy == nil {
f = v1.Fail

View File

@ -35,7 +35,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/cel"
)
var _ cel.Filter = &fakeCelFilter{}
var _ cel.ConditionEvaluator = &fakeCelFilter{}
type fakeCelFilter struct {
evaluations []cel.EvaluationResult