2023-06-01 16:58:10 +00:00
/ *
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"
2023-08-17 05:15:28 +00:00
v1 "k8s.io/api/core/v1"
2023-06-01 16:58:10 +00:00
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"
2023-08-17 05:15:28 +00:00
"k8s.io/apiserver/pkg/cel/environment"
2023-06-01 16:58:10 +00:00
"k8s.io/apiserver/pkg/cel/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
2023-08-17 05:15:28 +00:00
compiler Compiler
2023-06-01 16:58:10 +00:00
}
2023-08-17 05:15:28 +00:00
func NewFilterCompiler ( env * environment . EnvSet ) FilterCompiler {
return & filterCompiler { compiler : NewCompiler ( env ) }
2023-06-01 16:58:10 +00:00
}
type evaluationActivation struct {
2023-08-17 05:15:28 +00:00
object , oldObject , params , request , namespace , authorizer , requestResourceAuthorizer , variables interface { }
2023-06-01 16:58:10 +00:00
}
// 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
2023-08-17 05:15:28 +00:00
case NamespaceVarName :
return a . namespace , true
2023-06-01 16:58:10 +00:00
case AuthorizerVarName :
return a . authorizer , a . authorizer != nil
case RequestResourceAuthorizerVarName :
return a . requestResourceAuthorizer , a . requestResourceAuthorizer != nil
2023-08-17 05:15:28 +00:00
case VariableVarName : // variables always present
return a . variables , true
2023-06-01 16:58:10 +00:00
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
2023-08-17 05:15:28 +00:00
func ( c * filterCompiler ) Compile ( expressionAccessors [ ] ExpressionAccessor , options OptionalVariableDeclarations , mode environment . Type ) Filter {
2023-06-01 16:58:10 +00:00
compilationResults := make ( [ ] CompilationResult , len ( expressionAccessors ) )
for i , expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
2023-08-17 05:15:28 +00:00
compilationResults [ i ] = c . compiler . CompileCELExpression ( expressionAccessor , options , mode )
2023-06-01 16:58:10 +00:00
}
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.
2023-08-17 05:15:28 +00:00
func ( f * filter ) ForInput ( ctx context . Context , versionedAttr * admission . VersionedAttributes , request * admissionv1 . AdmissionRequest , inputs OptionalVariableBindings , namespace * v1 . Namespace , runtimeCELCostBudget int64 ) ( [ ] EvaluationResult , int64 , error ) {
2023-06-01 16:58:10 +00:00
// 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
}
2023-08-17 05:15:28 +00:00
namespaceVal , err := objectToResolveVal ( namespace )
if err != nil {
return nil , - 1 , err
}
2023-06-01 16:58:10 +00:00
va := & evaluationActivation {
object : objectVal ,
oldObject : oldObjectVal ,
params : paramsVal ,
request : requestVal . Object ,
2023-08-17 05:15:28 +00:00
namespace : namespaceVal ,
2023-06-01 16:58:10 +00:00
authorizer : authorizerVal ,
requestResourceAuthorizer : requestResourceAuthorizerVal ,
}
2023-08-17 05:15:28 +00:00
// 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 )
}
2023-06-01 16:58:10 +00:00
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 ) ,
}
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 )
2023-08-17 05:15:28 +00:00
// 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" ) ,
}
}
remainingBudget -= compositionCost
}
2023-06-01 16:58:10 +00:00
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 ( ) ) ,
}
} 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" ) ,
}
}
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
2023-08-17 05:15:28 +00:00
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
2023-06-01 16:58:10 +00:00
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 ( ) ,
} ,
}
}
2023-08-17 05:15:28 +00:00
// 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 ,
} ,
}
}
2023-06-01 16:58:10 +00:00
// 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
}