Add mutation support into CompositedCompiler and reorganize for clarity
Kubernetes-commit: 081353bf8ad963d43c5da6714a24f62cfe0b8401
This commit is contained in:
parent
9ead80d1bb
commit
0e6467b270
|
@ -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
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue