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 ( require (
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a 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-oidc v2.2.1+incompatible
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/emicklei/go-restful/v3 v3.11.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/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // 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/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect

View File

@ -18,11 +18,14 @@ package environment
import ( import (
"sort" "sort"
"strings"
"testing" "testing"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/library"
) )
// BenchmarkLoadBaseEnv is expected to be very fast, because a // 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 { func librariesInVersions(t *testing.T, vops ...VersionedOptions) []string {
env, err := cel.NewCustomEnv() env, err := cel.NewCustomEnv()
if err != nil { if err != nil {

View File

@ -41,11 +41,11 @@ type Format struct {
MaxRegexSize int 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) 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 { switch typeVal {
case FormatType: case FormatType:
return d return d
@ -56,18 +56,18 @@ func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
} }
} }
func (d *Format) Equal(other ref.Val) ref.Val { func (d Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(*Format) otherDur, ok := other.(Format)
if !ok { if !ok {
return types.MaybeNoSuchOverloadErr(other) return types.MaybeNoSuchOverloadErr(other)
} }
return types.Bool(d.Name == otherDur.Name) return types.Bool(d.Name == otherDur.Name)
} }
func (d *Format) Type() ref.Type { func (d Format) Type() ref.Type {
return FormatType return FormatType
} }
func (d *Format) Value() interface{} { func (d Format) Value() interface{} {
return d return d
} }

View File

@ -232,7 +232,20 @@ var authzLib = &authz{}
type authz struct{} type authz struct{}
func (*authz) LibraryName() string { 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{ var authzLibraryDecls = map[string][]cel.FunctionOpt{
@ -324,7 +337,15 @@ var authzSelectorsLib = &authzSelectors{}
type authzSelectors struct{} type authzSelectors struct{}
func (*authzSelectors) LibraryName() string { 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{ var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -109,7 +109,15 @@ var cidrsLib = &cidrs{}
type cidrs struct{} type cidrs struct{}
func (*cidrs) LibraryName() string { 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{ var cidrLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -18,17 +18,14 @@ package library
import ( import (
"fmt" "fmt"
"math"
"reflect"
"github.com/google/cel-go/checker" "github.com/google/cel-go/checker"
"github.com/google/cel-go/common" "github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
"math"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel"
) )
@ -50,22 +47,6 @@ var knownUnhandledFunctions = map[string]bool{
"strings.quote": true, "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. // CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
type CostEstimator struct { type CostEstimator struct {
// SizeEstimator provides a CostEstimator.EstimateSize that this CostEstimator will delegate size estimation // 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": case "validate":
if len(args) >= 2 { if len(args) >= 2 {
format, isFormat := args[0].Value().(*cel.Format) format, isFormat := args[0].Value().(cel.Format)
if isFormat { if isFormat {
strSize := actualSize(args[1]) strSize := actualSize(args[1])
@ -258,18 +239,17 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
unitCost := uint64(1) unitCost := uint64(1)
lhs := args[0] lhs := args[0]
switch lhs.(type) { switch lhs.(type) {
case cel.Quantity: case *cel.Quantity, cel.Quantity,
return &unitCost *cel.IP, cel.IP,
case cel.IP: *cel.CIDR, cel.CIDR,
return &unitCost *cel.Format, cel.Format, // Formats have a small max size. Format takes pointer receiver.
case cel.CIDR: *cel.URL, cel.URL, // TODO: Computing the actual cost is expensive, and changing this would be a breaking change
return &unitCost *cel.Semver, cel.Semver,
case *cel.Format: // Formats have a small max size. *authorizerVal, authorizerVal, *pathCheckVal, pathCheckVal, *groupCheckVal, groupCheckVal,
return &unitCost *resourceCheckVal, resourceCheckVal, *decisionVal, decisionVal:
case cel.URL: // TODO: Computing the actual cost is expensive, and changing this would be a breaking change
return &unitCost return &unitCost
default: 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)) 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 { if t.Kind() == types.StructKind {
switch t { 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}} return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case cel.FormatType: case cel.FormatType:
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: cel.MaxFormatSize}.MultiplyByCostFactor(common.StringTraversalCostFactor)} 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)} 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)) panic(fmt.Errorf("EstimateCallCost: unhandled equality for Kubernetes type %v", t))
} }
} }

View File

@ -19,6 +19,7 @@ package library
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/google/cel-go/common/types/ref"
"testing" "testing"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
@ -30,6 +31,7 @@ import (
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
) )
const ( const (
@ -1231,10 +1233,10 @@ func TestSize(t *testing.T) {
est := &CostEstimator{SizeEstimator: &testCostEstimator{}} est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { 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)) argNodes := make([]checker.AstNode, len(tc.argSizes))
for i, arg := range 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) result := est.EstimateCallCost(tc.function, tc.overload, &targetNode, argNodes)
if result.ResultSize == nil { 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 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 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 return nil // not needed
} }
func (t testSizeNode) Expr() ast.Expr { func (t testNode) ComputedSize() *checker.SizeEstimate {
return nil // not needed
}
func (t testSizeNode) ComputedSize() *checker.SizeEstimate {
return &t.size return &t.size
} }

View File

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

View File

@ -132,7 +132,15 @@ var ipLib = &ip{}
type ip struct{} type ip struct{}
func (*ip) LibraryName() string { 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{ 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 package library
import ( import (
"testing"
"github.com/google/cel-go/cel" "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" "k8s.io/apimachinery/pkg/util/sets"
) )
func TestLibraryCompatibility(t *testing.T) { 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]() functionNames := sets.New[string]()
for _, lib := range libs { for _, lib := range KnownLibraries() {
for name := range lib { 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{}{} functionNames[name] = struct{}{}
} }
} }
@ -50,7 +53,7 @@ func TestLibraryCompatibility(t *testing.T) {
// Kubernetes <1.30>: // Kubernetes <1.30>:
"ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string", "ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string",
// Kubernetes <1.31>: // Kubernetes <1.31>:
"fieldSelector", "labelSelector", "validate", "format.named", "fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver",
// Kubernetes <1.??>: // 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) 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{} type lists struct{}
func (*lists) LibraryName() string { 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") var paramA = cel.TypeParamType("A")

View File

@ -143,7 +143,15 @@ var quantityLib = &quantity{}
type quantity struct{} type quantity struct{}
func (*quantity) LibraryName() string { 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{ var quantityLibraryDecls = map[string][]cel.FunctionOpt{

View File

@ -52,7 +52,15 @@ var regexLib = &regex{}
type regex struct{} type regex struct{}
func (*regex) LibraryName() string { 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{ 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. limitations under the License.
*/ */
package cel_test package library_test
import ( import (
"regexp" "regexp"
@ -27,7 +27,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/sets" "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) { 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", name: "parse",
expr: `semver("1.2.3")`, 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", name: "parseInvalidVersion",

View File

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

View File

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

View File

@ -113,7 +113,15 @@ var urlsLib = &urls{}
type urls struct{} type urls struct{}
func (*urls) LibraryName() string { 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{ var urlLibraryDecls = map[string][]cel.FunctionOpt{

View File

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