k8s.io/apiserver@v0.31.1/pkg/cel/library/lists.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package library
    18  
    19  import (
    20  	"fmt"
    21  
    22  	"github.com/google/cel-go/cel"
    23  	"github.com/google/cel-go/common/types"
    24  	"github.com/google/cel-go/common/types/ref"
    25  	"github.com/google/cel-go/common/types/traits"
    26  	"github.com/google/cel-go/interpreter/functions"
    27  )
    28  
    29  // Lists provides a CEL function library extension of list utility functions.
    30  //
    31  // isSorted
    32  //
    33  // Returns true if the provided list of comparable elements is sorted, else returns false.
    34  //
    35  //	<list<T>>.isSorted() <bool>, T must be a comparable type
    36  //
    37  // Examples:
    38  //
    39  //	[1, 2, 3].isSorted()  // return true
    40  //	['a', 'b', 'b', 'c'].isSorted()  // return true
    41  //	[2.0, 1.0].isSorted()  // return false
    42  //	[1].isSorted()  // return true
    43  //	[].isSorted()  // return true
    44  //
    45  // sum
    46  //
    47  // Returns the sum of the elements of the provided list. Supports CEL number (int, uint, double) and duration types.
    48  //
    49  //	<list<T>>.sum() <T>, T must be a numeric type or a duration
    50  //
    51  // Examples:
    52  //
    53  //	[1, 3].sum() // returns 4
    54  //	[1.0, 3.0].sum() // returns 4.0
    55  //	['1m', '1s'].sum() // returns '1m1s'
    56  //	emptyIntList.sum() // returns 0
    57  //	emptyDoubleList.sum() // returns 0.0
    58  //	[].sum() // returns 0
    59  //
    60  // min / max
    61  //
    62  // Returns the minimum/maximum valued element of the provided list. Supports all comparable types.
    63  // If the list is empty, an error is returned.
    64  //
    65  //	<list<T>>.min() <T>, T must be a comparable type
    66  //	<list<T>>.max() <T>, T must be a comparable type
    67  //
    68  // Examples:
    69  //
    70  //	[1, 3].min() // returns 1
    71  //	[1, 3].max() // returns 3
    72  //	[].min() // error
    73  //	[1].min() // returns 1
    74  //	([0] + emptyList).min() // returns 0
    75  //
    76  // indexOf / lastIndexOf
    77  //
    78  // Returns either the first or last positional index of the provided element in the list.
    79  // If the element is not found, -1 is returned. Supports all equatable types.
    80  //
    81  //	<list<T>>.indexOf(<T>) <int>, T must be an equatable type
    82  //	<list<T>>.lastIndexOf(<T>) <int>, T must be an equatable type
    83  //
    84  // Examples:
    85  //
    86  //	[1, 2, 2, 3].indexOf(2) // returns 1
    87  //	['a', 'b', 'b', 'c'].lastIndexOf('b') // returns 2
    88  //	[1.0].indexOf(1.1) // returns -1
    89  //	[].indexOf('string') // returns -1
    90  func Lists() cel.EnvOption {
    91  	return cel.Lib(listsLib)
    92  }
    93  
    94  var listsLib = &lists{}
    95  
    96  type lists struct{}
    97  
    98  func (*lists) LibraryName() string {
    99  	return "k8s.lists"
   100  }
   101  
   102  var paramA = cel.TypeParamType("A")
   103  
   104  // CEL typeParams can be used to constraint to a specific trait (e.g. traits.ComparableType) if the 1st operand is the type to constrain.
   105  // But the functions we need to constrain are <list<paramType>>, not just <paramType>.
   106  // Make sure the order of overload set is deterministic
   107  type namedCELType struct {
   108  	typeName string
   109  	celType  *cel.Type
   110  }
   111  
   112  var summableTypes = []namedCELType{
   113  	{typeName: "int", celType: cel.IntType},
   114  	{typeName: "uint", celType: cel.UintType},
   115  	{typeName: "double", celType: cel.DoubleType},
   116  	{typeName: "duration", celType: cel.DurationType},
   117  }
   118  
   119  var zeroValuesOfSummableTypes = map[string]ref.Val{
   120  	"int":      types.Int(0),
   121  	"uint":     types.Uint(0),
   122  	"double":   types.Double(0.0),
   123  	"duration": types.Duration{Duration: 0},
   124  }
   125  var comparableTypes = []namedCELType{
   126  	{typeName: "int", celType: cel.IntType},
   127  	{typeName: "uint", celType: cel.UintType},
   128  	{typeName: "double", celType: cel.DoubleType},
   129  	{typeName: "bool", celType: cel.BoolType},
   130  	{typeName: "duration", celType: cel.DurationType},
   131  	{typeName: "timestamp", celType: cel.TimestampType},
   132  	{typeName: "string", celType: cel.StringType},
   133  	{typeName: "bytes", celType: cel.BytesType},
   134  }
   135  
   136  // WARNING: All library additions or modifications must follow
   137  // https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2876-crd-validation-expression-language#function-library-updates
   138  var listsLibraryDecls = map[string][]cel.FunctionOpt{
   139  	"isSorted": templatedOverloads(comparableTypes, func(name string, paramType *cel.Type) cel.FunctionOpt {
   140  		return cel.MemberOverload(fmt.Sprintf("list_%s_is_sorted_bool", name),
   141  			[]*cel.Type{cel.ListType(paramType)}, cel.BoolType, cel.UnaryBinding(isSorted))
   142  	}),
   143  	"sum": templatedOverloads(summableTypes, func(name string, paramType *cel.Type) cel.FunctionOpt {
   144  		return cel.MemberOverload(fmt.Sprintf("list_%s_sum_%s", name, name),
   145  			[]*cel.Type{cel.ListType(paramType)}, paramType, cel.UnaryBinding(func(list ref.Val) ref.Val {
   146  				return sum(
   147  					func() ref.Val {
   148  						return zeroValuesOfSummableTypes[name]
   149  					})(list)
   150  			}))
   151  	}),
   152  	"max": templatedOverloads(comparableTypes, func(name string, paramType *cel.Type) cel.FunctionOpt {
   153  		return cel.MemberOverload(fmt.Sprintf("list_%s_max_%s", name, name),
   154  			[]*cel.Type{cel.ListType(paramType)}, paramType, cel.UnaryBinding(max()))
   155  	}),
   156  	"min": templatedOverloads(comparableTypes, func(name string, paramType *cel.Type) cel.FunctionOpt {
   157  		return cel.MemberOverload(fmt.Sprintf("list_%s_min_%s", name, name),
   158  			[]*cel.Type{cel.ListType(paramType)}, paramType, cel.UnaryBinding(min()))
   159  	}),
   160  	"indexOf": {
   161  		cel.MemberOverload("list_a_index_of_int", []*cel.Type{cel.ListType(paramA), paramA}, cel.IntType,
   162  			cel.BinaryBinding(indexOf)),
   163  	},
   164  	"lastIndexOf": {
   165  		cel.MemberOverload("list_a_last_index_of_int", []*cel.Type{cel.ListType(paramA), paramA}, cel.IntType,
   166  			cel.BinaryBinding(lastIndexOf)),
   167  	},
   168  }
   169  
   170  func (*lists) CompileOptions() []cel.EnvOption {
   171  	options := []cel.EnvOption{}
   172  	for name, overloads := range listsLibraryDecls {
   173  		options = append(options, cel.Function(name, overloads...))
   174  	}
   175  	return options
   176  }
   177  
   178  func (*lists) ProgramOptions() []cel.ProgramOption {
   179  	return []cel.ProgramOption{}
   180  }
   181  
   182  func isSorted(val ref.Val) ref.Val {
   183  	var prev traits.Comparer
   184  	iterable, ok := val.(traits.Iterable)
   185  	if !ok {
   186  		return types.MaybeNoSuchOverloadErr(val)
   187  	}
   188  	for it := iterable.Iterator(); it.HasNext() == types.True; {
   189  		next := it.Next()
   190  		nextCmp, ok := next.(traits.Comparer)
   191  		if !ok {
   192  			return types.MaybeNoSuchOverloadErr(next)
   193  		}
   194  		if prev != nil {
   195  			cmp := prev.Compare(next)
   196  			if cmp == types.IntOne {
   197  				return types.False
   198  			}
   199  		}
   200  		prev = nextCmp
   201  	}
   202  	return types.True
   203  }
   204  
   205  func sum(init func() ref.Val) functions.UnaryOp {
   206  	return func(val ref.Val) ref.Val {
   207  		i := init()
   208  		acc, ok := i.(traits.Adder)
   209  		if !ok {
   210  			// Should never happen since all passed in init values are valid
   211  			return types.MaybeNoSuchOverloadErr(i)
   212  		}
   213  		iterable, ok := val.(traits.Iterable)
   214  		if !ok {
   215  			return types.MaybeNoSuchOverloadErr(val)
   216  		}
   217  		for it := iterable.Iterator(); it.HasNext() == types.True; {
   218  			next := it.Next()
   219  			nextAdder, ok := next.(traits.Adder)
   220  			if !ok {
   221  				// Should never happen for type checked CEL programs
   222  				return types.MaybeNoSuchOverloadErr(next)
   223  			}
   224  			if acc != nil {
   225  				s := acc.Add(next)
   226  				sum, ok := s.(traits.Adder)
   227  				if !ok {
   228  					// Should never happen for type checked CEL programs
   229  					return types.MaybeNoSuchOverloadErr(s)
   230  				}
   231  				acc = sum
   232  			} else {
   233  				acc = nextAdder
   234  			}
   235  		}
   236  		return acc.(ref.Val)
   237  	}
   238  }
   239  
   240  func min() functions.UnaryOp {
   241  	return cmp("min", types.IntOne)
   242  }
   243  
   244  func max() functions.UnaryOp {
   245  	return cmp("max", types.IntNegOne)
   246  }
   247  
   248  func cmp(opName string, opPreferCmpResult ref.Val) functions.UnaryOp {
   249  	return func(val ref.Val) ref.Val {
   250  		var result traits.Comparer
   251  		iterable, ok := val.(traits.Iterable)
   252  		if !ok {
   253  			return types.MaybeNoSuchOverloadErr(val)
   254  		}
   255  		for it := iterable.Iterator(); it.HasNext() == types.True; {
   256  			next := it.Next()
   257  			nextCmp, ok := next.(traits.Comparer)
   258  			if !ok {
   259  				// Should never happen for type checked CEL programs
   260  				return types.MaybeNoSuchOverloadErr(next)
   261  			}
   262  			if result == nil {
   263  				result = nextCmp
   264  			} else {
   265  				cmp := result.Compare(next)
   266  				if cmp == opPreferCmpResult {
   267  					result = nextCmp
   268  				}
   269  			}
   270  		}
   271  		if result == nil {
   272  			return types.NewErr("%s called on empty list", opName)
   273  		}
   274  		return result.(ref.Val)
   275  	}
   276  }
   277  
   278  func indexOf(list ref.Val, item ref.Val) ref.Val {
   279  	lister, ok := list.(traits.Lister)
   280  	if !ok {
   281  		return types.MaybeNoSuchOverloadErr(list)
   282  	}
   283  	sz := lister.Size().(types.Int)
   284  	for i := types.Int(0); i < sz; i++ {
   285  		if lister.Get(types.Int(i)).Equal(item) == types.True {
   286  			return types.Int(i)
   287  		}
   288  	}
   289  	return types.Int(-1)
   290  }
   291  
   292  func lastIndexOf(list ref.Val, item ref.Val) ref.Val {
   293  	lister, ok := list.(traits.Lister)
   294  	if !ok {
   295  		return types.MaybeNoSuchOverloadErr(list)
   296  	}
   297  	sz := lister.Size().(types.Int)
   298  	for i := sz - 1; i >= 0; i-- {
   299  		if lister.Get(types.Int(i)).Equal(item) == types.True {
   300  			return types.Int(i)
   301  		}
   302  	}
   303  	return types.Int(-1)
   304  }
   305  
   306  // templatedOverloads returns overloads for each of the provided types. The template function is called with each type
   307  // name (map key) and type to construct the overloads.
   308  func templatedOverloads(types []namedCELType, template func(name string, t *cel.Type) cel.FunctionOpt) []cel.FunctionOpt {
   309  	overloads := make([]cel.FunctionOpt, len(types))
   310  	i := 0
   311  	for _, t := range types {
   312  		overloads[i] = template(t.typeName, t.celType)
   313  		i++
   314  	}
   315  	return overloads
   316  }