Move CEL semver library into common libs, fix cost tests to use registered types

This commit is contained in:
Joe Betz 2024-09-10 16:55:57 -04:00
parent 0a2dfba067
commit e085f3818a
13 changed files with 65 additions and 54 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
@ -63,7 +64,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

@ -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

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

View File

@ -25,7 +25,6 @@ import (
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"math"
"strings"
"k8s.io/apiserver/pkg/cel"
)
@ -201,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])
@ -243,13 +242,14 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
case *cel.Quantity, cel.Quantity,
*cel.IP, cel.IP,
*cel.CIDR, cel.CIDR,
*cel.Format, // Formats have a small max size. Format takes pointer receiver.
*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 && isKubernetesType(lhs.Type()) {
if panicOnUnknown && lhs.Type() != nil && isRegisteredType(lhs.Type().TypeName()) {
panic(fmt.Errorf("CallCost: unhandled equality for Kubernetes type %T", lhs))
}
}
@ -509,7 +509,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
if t.Kind() == types.StructKind {
switch t {
case cel.QuantityType, AuthorizerType, PathCheckType, // O(1) cost equality checks
GroupCheckType, ResourceCheckType, DecisionType:
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)}
@ -523,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 && isKubernetesType(t) {
if panicOnUnknown && isRegisteredType(t.TypeName()) {
panic(fmt.Errorf("EstimateCallCost: unhandled equality for Kubernetes type %v", t))
}
}
@ -632,17 +632,3 @@ func traversalCost(v ref.Val) uint64 {
return 1
}
}
// isKubernetesType returns ture if a type is type defined by Kubernetes,
// as identified by opaque or struct types with a "kubernetes." prefix.
func isKubernetesType(t ref.Type) bool {
if tt, ok := t.(*types.Type); ok {
switch tt.Kind() {
case types.OpaqueKind, types.StructKind:
return strings.HasPrefix(tt.TypeName(), "kubernetes.")
default:
return false
}
}
return false
}

View File

@ -1262,7 +1262,8 @@ func TestTypeEquality(t *testing.T) {
"kubernetes.Quantity": apiservercel.Quantity{},
"net.IP": apiservercel.IP{},
"net.CIDR": apiservercel.CIDR{},
"kubernetes.NamedFormat": &apiservercel.Format{},
"kubernetes.NamedFormat": apiservercel.Format{},
"kubernetes.Semver": apiservercel.Semver{},
}
originalPanicOnUnknown := panicOnUnknown

View File

@ -133,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) },
@ -261,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,7 @@ var ipLib = &ip{}
type ip struct{}
func (*ip) LibraryName() string {
return "net.ip"
return "kubernetes.net.ip"
}
func (*ip) declarations() map[string][]cel.FunctionOpt {

View File

@ -1,5 +1,5 @@
/*
Copyright 2023 The Kubernetes Authors.
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.
@ -16,7 +16,9 @@ limitations under the License.
package library
import "github.com/google/cel-go/cel"
import (
"github.com/google/cel-go/cel"
)
// Library represents a CEL library used by kubernetes.
type Library interface {
@ -42,5 +44,17 @@ func KnownLibraries() []Library {
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

@ -49,7 +49,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.??>:
)
@ -101,5 +101,4 @@ func TestTypeRegistration(t *testing.T) {
t.Errorf("Expected types to be registered with the %s library Type() functions, but they were not: %v", lib.LibraryName(), unregistered)
}
}
}

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

@ -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
//