Merge pull request #126368 from jpbetz/organize-cel-libraries

Improve structure of CEL libraries to ensure cost tests kept accurate with introduction of new types
This commit is contained in:
Kubernetes Prow Robot 2024-09-11 20:41:19 +01:00 committed by GitHub
commit e3a81ab000
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 330 additions and 88 deletions

View File

@ -6,6 +6,7 @@ go 1.22.0
require (
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
github.com/blang/semver/v4 v4.0.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-systemd/v22 v22.5.0
github.com/emicklei/go-restful/v3 v3.11.0
@ -64,7 +65,6 @@ require (
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect

View File

@ -18,11 +18,14 @@ package environment
import (
"sort"
"strings"
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/library"
)
// BenchmarkLoadBaseEnv is expected to be very fast, because a
@ -112,6 +115,29 @@ func TestLibraryCoverage(t *testing.T) {
}
}
// TestKnownLibraries ensures that all libraries used in the base environment are also registered with
// KnownLibraries. Other tests rely on KnownLibraries to provide an up-to-date list of CEL libraries.
func TestKnownLibraries(t *testing.T) {
known := sets.New[string]()
used := sets.New[string]()
for _, lib := range library.KnownLibraries() {
known.Insert(lib.LibraryName())
}
for _, libName := range MustBaseEnvSet(version.MajorMinor(1, 0), true).storedExpressions.Libraries() {
if strings.HasPrefix(libName, "cel.lib") { // ignore core libs
continue
}
used.Insert(libName)
}
unexpected := used.Difference(known)
if len(unexpected) != 0 {
t.Errorf("Expected all libraries in the base environment to be included in k8s.io/apiserver/pkg/cel/library's KnownLibraries, but found missing libraries: %v", unexpected)
}
}
func librariesInVersions(t *testing.T, vops ...VersionedOptions) []string {
env, err := cel.NewCustomEnv()
if err != nil {

View File

@ -41,11 +41,11 @@ type Format struct {
MaxRegexSize int
}
func (d *Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
func (d Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion error from 'Format' to '%v'", typeDesc)
}
func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
func (d Format) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case FormatType:
return d
@ -56,18 +56,18 @@ func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
}
}
func (d *Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(*Format)
func (d Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(Format)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(d.Name == otherDur.Name)
}
func (d *Format) Type() ref.Type {
func (d Format) Type() ref.Type {
return FormatType
}
func (d *Format) Value() interface{} {
func (d Format) Value() interface{} {
return d
}

View File

@ -232,7 +232,20 @@ var authzLib = &authz{}
type authz struct{}
func (*authz) LibraryName() string {
return "k8s.authz"
return "kubernetes.authz"
}
func (*authz) Types() []*cel.Type {
return []*cel.Type{
AuthorizerType,
PathCheckType,
GroupCheckType,
ResourceCheckType,
DecisionType}
}
func (*authz) declarations() map[string][]cel.FunctionOpt {
return authzLibraryDecls
}
var authzLibraryDecls = map[string][]cel.FunctionOpt{
@ -324,7 +337,15 @@ var authzSelectorsLib = &authzSelectors{}
type authzSelectors struct{}
func (*authzSelectors) LibraryName() string {
return "k8s.authzSelectors"
return "kubernetes.authzSelectors"
}
func (*authzSelectors) Types() []*cel.Type {
return []*cel.Type{ResourceCheckType}
}
func (*authzSelectors) declarations() map[string][]cel.FunctionOpt {
return authzSelectorsLibraryDecls
}
var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -109,7 +109,15 @@ var cidrsLib = &cidrs{}
type cidrs struct{}
func (*cidrs) LibraryName() string {
return "net.cidr"
return "kubernetes.net.cidr"
}
func (*cidrs) declarations() map[string][]cel.FunctionOpt {
return cidrLibraryDecls
}
func (*cidrs) Types() []*cel.Type {
return []*cel.Type{apiservercel.CIDRType, apiservercel.IPType}
}
var cidrLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -18,17 +18,14 @@ package library
import (
"fmt"
"math"
"reflect"
"github.com/google/cel-go/checker"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"math"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/cel"
)
@ -50,22 +47,6 @@ var knownUnhandledFunctions = map[string]bool{
"strings.quote": true,
}
// TODO: Replace this with a utility that extracts types from libraries.
var knownKubernetesRuntimeTypes = sets.New[reflect.Type](
reflect.ValueOf(cel.URL{}).Type(),
reflect.ValueOf(cel.IP{}).Type(),
reflect.ValueOf(cel.CIDR{}).Type(),
reflect.ValueOf(&cel.Format{}).Type(),
reflect.ValueOf(cel.Quantity{}).Type(),
)
var knownKubernetesCompilerTypes = sets.New[ref.Type](
cel.CIDRType,
cel.IPType,
cel.FormatType,
cel.QuantityType,
cel.URLType,
)
// CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
type CostEstimator struct {
// SizeEstimator provides a CostEstimator.EstimateSize that this CostEstimator will delegate size estimation
@ -219,7 +200,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
}
case "validate":
if len(args) >= 2 {
format, isFormat := args[0].Value().(*cel.Format)
format, isFormat := args[0].Value().(cel.Format)
if isFormat {
strSize := actualSize(args[1])
@ -258,18 +239,17 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
unitCost := uint64(1)
lhs := args[0]
switch lhs.(type) {
case cel.Quantity:
return &unitCost
case cel.IP:
return &unitCost
case cel.CIDR:
return &unitCost
case *cel.Format: // Formats have a small max size.
return &unitCost
case cel.URL: // TODO: Computing the actual cost is expensive, and changing this would be a breaking change
case *cel.Quantity, cel.Quantity,
*cel.IP, cel.IP,
*cel.CIDR, cel.CIDR,
*cel.Format, cel.Format, // Formats have a small max size. Format takes pointer receiver.
*cel.URL, cel.URL, // TODO: Computing the actual cost is expensive, and changing this would be a breaking change
*cel.Semver, cel.Semver,
*authorizerVal, authorizerVal, *pathCheckVal, pathCheckVal, *groupCheckVal, groupCheckVal,
*resourceCheckVal, resourceCheckVal, *decisionVal, decisionVal:
return &unitCost
default:
if panicOnUnknown && knownKubernetesRuntimeTypes.Has(reflect.ValueOf(lhs).Type()) {
if panicOnUnknown && lhs.Type() != nil && isRegisteredType(lhs.Type().TypeName()) {
panic(fmt.Errorf("CallCost: unhandled equality for Kubernetes type %T", lhs))
}
}
@ -528,7 +508,8 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
}
if t.Kind() == types.StructKind {
switch t {
case cel.QuantityType: // O(1) cost equality checks
case cel.QuantityType, AuthorizerType, PathCheckType, // O(1) cost equality checks
GroupCheckType, ResourceCheckType, DecisionType, cel.SemverType:
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case cel.FormatType:
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: cel.MaxFormatSize}.MultiplyByCostFactor(common.StringTraversalCostFactor)}
@ -542,7 +523,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: size.Max}.MultiplyByCostFactor(common.StringTraversalCostFactor)}
}
}
if panicOnUnknown && knownKubernetesCompilerTypes.Has(t) {
if panicOnUnknown && isRegisteredType(t.TypeName()) {
panic(fmt.Errorf("EstimateCallCost: unhandled equality for Kubernetes type %v", t))
}
}

View File

@ -19,6 +19,7 @@ package library
import (
"context"
"fmt"
"github.com/google/cel-go/common/types/ref"
"testing"
"github.com/google/cel-go/cel"
@ -30,6 +31,7 @@ import (
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
)
const (
@ -1231,10 +1233,10 @@ func TestSize(t *testing.T) {
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var targetNode checker.AstNode = testSizeNode{size: tc.targetSize}
var targetNode checker.AstNode = testNode{size: tc.targetSize}
argNodes := make([]checker.AstNode, len(tc.argSizes))
for i, arg := range tc.argSizes {
argNodes[i] = testSizeNode{size: arg}
argNodes[i] = testNode{size: arg}
}
result := est.EstimateCallCost(tc.function, tc.overload, &targetNode, argNodes)
if result.ResultSize == nil {
@ -1247,25 +1249,63 @@ func TestSize(t *testing.T) {
}
}
type testSizeNode struct {
// TestTypeEquality ensures that cost is tested for all custom types used by Kubernetes libraries.
func TestTypeEquality(t *testing.T) {
examples := map[string]ref.Val{
// Add example ref.Val's for custom types in Kubernetes here:
"kubernetes.authorization.Authorizer": authorizerVal{},
"kubernetes.authorization.PathCheck": pathCheckVal{},
"kubernetes.authorization.GroupCheck": groupCheckVal{},
"kubernetes.authorization.ResourceCheck": resourceCheckVal{},
"kubernetes.authorization.Decision": decisionVal{},
"kubernetes.URL": apiservercel.URL{},
"kubernetes.Quantity": apiservercel.Quantity{},
"net.IP": apiservercel.IP{},
"net.CIDR": apiservercel.CIDR{},
"kubernetes.NamedFormat": apiservercel.Format{},
"kubernetes.Semver": apiservercel.Semver{},
}
originalPanicOnUnknown := panicOnUnknown
panicOnUnknown = true
t.Cleanup(func() { panicOnUnknown = originalPanicOnUnknown })
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
for _, lib := range KnownLibraries() {
for _, kt := range lib.Types() {
t.Run(kt.TypeName(), func(t *testing.T) {
typeNode := testNode{size: checker.SizeEstimate{Min: 10, Max: 100}, typ: kt}
est.EstimateCallCost("_==_", "", nil, []checker.AstNode{typeNode, typeNode})
ex, ok := examples[kt.TypeName()]
if !ok {
t.Errorf("missing example for type: %s", kt.TypeName())
}
est.CallCost("_==_", "", []ref.Val{ex, ex}, nil)
})
}
}
}
type testNode struct {
size checker.SizeEstimate
typ *types.Type
}
var _ checker.AstNode = (*testSizeNode)(nil)
var _ checker.AstNode = (*testNode)(nil)
func (t testSizeNode) Path() []string {
func (t testNode) Path() []string {
return nil // not needed
}
func (t testSizeNode) Type() *types.Type {
func (t testNode) Type() *types.Type {
return t.typ // not needed
}
func (t testNode) Expr() ast.Expr {
return nil // not needed
}
func (t testSizeNode) Expr() ast.Expr {
return nil // not needed
}
func (t testSizeNode) ComputedSize() *checker.SizeEstimate {
func (t testNode) ComputedSize() *checker.SizeEstimate {
return &t.size
}

View File

@ -25,6 +25,7 @@ import (
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/validation"
apiservercel "k8s.io/apiserver/pkg/cel"
@ -90,7 +91,15 @@ var formatLib = &format{}
type format struct{}
func (*format) LibraryName() string {
return "format"
return "kubernetes.format"
}
func (*format) Types() []*cel.Type {
return []*cel.Type{apiservercel.FormatType}
}
func (*format) declarations() map[string][]cel.FunctionOpt {
return formatLibraryDecls
}
func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt {
@ -124,7 +133,7 @@ func (*format) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{
var ConstantFormats = map[string]apiservercel.Format{
"dns1123Label": {
Name: "DNS1123Label",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) },
@ -252,7 +261,7 @@ var formatLibraryDecls = map[string][]cel.FunctionOpt{
}
func formatValidate(arg1, arg2 ref.Val) ref.Val {
f, ok := arg1.Value().(*apiservercel.Format)
f, ok := arg1.Value().(apiservercel.Format)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}

View File

@ -132,7 +132,15 @@ var ipLib = &ip{}
type ip struct{}
func (*ip) LibraryName() string {
return "net.ip"
return "kubernetes.net.ip"
}
func (*ip) declarations() map[string][]cel.FunctionOpt {
return ipLibraryDecls
}
func (*ip) Types() []*cel.Type {
return []*cel.Type{apiservercel.IPType}
}
var ipLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -0,0 +1,60 @@
/*
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 library
import (
"github.com/google/cel-go/cel"
)
// Library represents a CEL library used by kubernetes.
type Library interface {
// SingletonLibrary provides the library name and ensures the library can be safely registered into environments.
cel.SingletonLibrary
// Types provides all custom types introduced by the library.
Types() []*cel.Type
// declarations returns all function declarations provided by the library.
declarations() map[string][]cel.FunctionOpt
}
// KnownLibraries returns all libraries used in Kubernetes.
func KnownLibraries() []Library {
return []Library{
authzLib,
authzSelectorsLib,
listsLib,
regexLib,
urlsLib,
quantityLib,
ipLib,
cidrsLib,
formatLib,
semverLib,
}
}
func isRegisteredType(typeName string) bool {
for _, lib := range KnownLibraries() {
for _, rt := range lib.Types() {
if rt.TypeName() == typeName {
return true
}
}
}
return false
}

View File

@ -17,19 +17,22 @@ limitations under the License.
package library
import (
"testing"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/types"
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestLibraryCompatibility(t *testing.T) {
var libs []map[string][]cel.FunctionOpt
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls, formatLibraryDecls, authzSelectorsLibraryDecls)
functionNames := sets.New[string]()
for _, lib := range libs {
for name := range lib {
for _, lib := range KnownLibraries() {
if !strings.HasPrefix(lib.LibraryName(), "kubernetes.") {
t.Errorf("Expected all kubernetes CEL libraries to have a name package with a 'kubernetes.' prefix but got %v", lib.LibraryName())
}
for name := range lib.declarations() {
functionNames[name] = struct{}{}
}
}
@ -50,7 +53,7 @@ func TestLibraryCompatibility(t *testing.T) {
// Kubernetes <1.30>:
"ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string",
// Kubernetes <1.31>:
"fieldSelector", "labelSelector", "validate", "format.named",
"fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver",
// Kubernetes <1.??>:
)
@ -66,3 +69,46 @@ func TestLibraryCompatibility(t *testing.T) {
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the missing function names: %v", missing)
}
}
// TestTypeRegistration ensures that all custom types defined and used by Kubernetes CEL libraries
// are returned by library.Types(). Other tests depend on Types() to provide an up-to-date list of
// types declared in a library.
func TestTypeRegistration(t *testing.T) {
for _, lib := range KnownLibraries() {
registeredTypes := sets.New[*cel.Type]()
usedTypes := sets.New[*cel.Type]()
// scan all registered function declarations for the library
for _, fn := range lib.declarations() {
fn, err := decls.NewFunction("placeholder-not-used", fn...)
if err != nil {
t.Fatal(err)
}
for _, o := range fn.OverloadDecls() {
// ArgTypes include both the receiver type (if present) and
// all function argument types.
for _, at := range o.ArgTypes() {
switch at.Kind() {
// User defined types are either Opaque or Struct.
case types.OpaqueKind, types.StructKind:
usedTypes.Insert(at)
default:
// skip
}
}
}
}
for _, lb := range lib.Types() {
registeredTypes.Insert(lb)
if !strings.HasPrefix(lb.TypeName(), "kubernetes.") && !legacyTypeNames.Has(lb.TypeName()) {
t.Errorf("Expected all types in kubernetes CEL libraries to have a type name packaged with a 'kubernetes.' prefix but got %v", lb.TypeName())
}
}
unregistered := usedTypes.Difference(registeredTypes)
if len(unregistered) != 0 {
t.Errorf("Expected types to be registered with the %s library Type() functions, but they were not: %v", lib.LibraryName(), unregistered)
}
}
}
// TODO: Consider renaming these to "kubernetes.net.IP" and "kubernetes.net.CIDR" if we decide not to promote them to cel-go
var legacyTypeNames = sets.New[string]("net.IP", "net.CIDR")

View File

@ -96,7 +96,15 @@ var listsLib = &lists{}
type lists struct{}
func (*lists) LibraryName() string {
return "k8s.lists"
return "kubernetes.lists"
}
func (*lists) Types() []*cel.Type {
return []*cel.Type{}
}
func (*lists) declarations() map[string][]cel.FunctionOpt {
return listsLibraryDecls
}
var paramA = cel.TypeParamType("A")

View File

@ -143,7 +143,15 @@ var quantityLib = &quantity{}
type quantity struct{}
func (*quantity) LibraryName() string {
return "k8s.quantity"
return "kubernetes.quantity"
}
func (*quantity) Types() []*cel.Type {
return []*cel.Type{apiservercel.QuantityType}
}
func (*quantity) declarations() map[string][]cel.FunctionOpt {
return quantityLibraryDecls
}
var quantityLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -52,7 +52,15 @@ var regexLib = &regex{}
type regex struct{}
func (*regex) LibraryName() string {
return "k8s.regex"
return "kubernetes.regex"
}
func (*regex) Types() []*cel.Type {
return []*cel.Type{}
}
func (*regex) declarations() map[string][]cel.FunctionOpt {
return regexLibraryDecls
}
var regexLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel_test
package library_test
import (
"regexp"
@ -27,7 +27,8 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/sets"
library "k8s.io/dynamic-resource-allocation/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
library "k8s.io/apiserver/pkg/cel/library"
)
func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
@ -117,7 +118,7 @@ func TestSemver(t *testing.T) {
{
name: "parse",
expr: `semver("1.2.3")`,
expectValue: library.Semver{Version: semver.MustParse("1.2.3")},
expectValue: apiservercel.Semver{Version: semver.MustParse("1.2.3")},
},
{
name: "parseInvalidVersion",

View File

@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
package library
import (
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
)
// Semver provides a CEL function library extension for [semver.Version].
@ -91,38 +93,45 @@ var semverLib = &semverLibType{}
type semverLibType struct{}
func (*semverLibType) LibraryName() string {
return "k8s.semver"
return "kubernetes.Semver"
}
func (*semverLibType) CompileOptions() []cel.EnvOption {
// Defined in this function to avoid an initialization order problem.
semverLibraryDecls := map[string][]cel.FunctionOpt{
func (*semverLibType) Types() []*cel.Type {
return []*cel.Type{apiservercel.SemverType}
}
func (*semverLibType) declarations() map[string][]cel.FunctionOpt {
return map[string][]cel.FunctionOpt{
"semver": {
cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, SemverType, cel.UnaryBinding((stringToSemver))),
cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, apiservercel.SemverType, cel.UnaryBinding((stringToSemver))),
},
"isSemver": {
cel.Overload("is_semver_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isSemver)),
},
"isGreaterThan": {
cel.MemberOverload("semver_is_greater_than", []*cel.Type{SemverType, SemverType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
cel.MemberOverload("semver_is_greater_than", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
},
"isLessThan": {
cel.MemberOverload("semver_is_less_than", []*cel.Type{SemverType, SemverType}, cel.BoolType, cel.BinaryBinding(semverIsLessThan)),
cel.MemberOverload("semver_is_less_than", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.BoolType, cel.BinaryBinding(semverIsLessThan)),
},
"compareTo": {
cel.MemberOverload("semver_compare_to", []*cel.Type{SemverType, SemverType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
cel.MemberOverload("semver_compare_to", []*cel.Type{apiservercel.SemverType, apiservercel.SemverType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
},
"major": {
cel.MemberOverload("semver_major", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverMajor)),
cel.MemberOverload("semver_major", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMajor)),
},
"minor": {
cel.MemberOverload("semver_minor", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverMinor)),
cel.MemberOverload("semver_minor", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMinor)),
},
"patch": {
cel.MemberOverload("semver_patch", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)),
cel.MemberOverload("semver_patch", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)),
},
}
}
func (s *semverLibType) CompileOptions() []cel.EnvOption {
// Defined in this function to avoid an initialization order problem.
semverLibraryDecls := s.declarations()
options := make([]cel.EnvOption, 0, len(semverLibraryDecls))
for name, overloads := range semverLibraryDecls {
options = append(options, cel.Function(name, overloads...))
@ -168,7 +177,7 @@ func stringToSemver(arg ref.Val) ref.Val {
return types.WrapErr(err)
}
return Semver{Version: v}
return apiservercel.Semver{Version: v}
}
func semverMajor(arg ref.Val) ref.Val {

View File

@ -38,7 +38,7 @@ type testLib struct {
}
func (*testLib) LibraryName() string {
return "k8s.test"
return "kubernetes.test"
}
type TestOption func(*testLib) *testLib

View File

@ -113,7 +113,15 @@ var urlsLib = &urls{}
type urls struct{}
func (*urls) LibraryName() string {
return "k8s.urls"
return "kubernetes.urls"
}
func (*urls) Types() []*cel.Type {
return []*cel.Type{apiservercel.URLType}
}
func (*urls) declarations() map[string][]cel.FunctionOpt {
return urlLibraryDecls
}
var urlLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -37,6 +37,7 @@ import (
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
const (
@ -151,7 +152,7 @@ func getAttributeValue(attr resourceapi.DeviceAttribute) (any, error) {
if err != nil {
return nil, fmt.Errorf("parse semantic version: %w", err)
}
return Semver{Version: v}, nil
return apiservercel.Semver{Version: v}, nil
default:
return nil, errors.New("unsupported attribute value")
}
@ -236,7 +237,7 @@ func mustBuildEnv() *environment.EnvSet {
EnvOptions: []cel.EnvOption{
cel.Variable(deviceVar, deviceType.CelType()),
SemverLib(),
library.SemverLib(),
// https://pkg.go.dev/github.com/google/cel-go/ext#Bindings
//