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 Kubernetes-commit: e3a81ab0005973f25d498308702f826795c6d8c2
This commit is contained in:
commit
438d2ea372
4
go.mod
4
go.mod
|
|
@ -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
|
||||||
|
|
@ -51,7 +52,7 @@ require (
|
||||||
k8s.io/client-go v0.0.0-20240910233403-8d0bbdfdcc60
|
k8s.io/client-go v0.0.0-20240910233403-8d0bbdfdcc60
|
||||||
k8s.io/component-base v0.0.0-20240906193739-83f63c39727c
|
k8s.io/component-base v0.0.0-20240906193739-83f63c39727c
|
||||||
k8s.io/klog/v2 v2.130.1
|
k8s.io/klog/v2 v2.130.1
|
||||||
k8s.io/kms v0.0.0-20240827234606-4aa59ca2ab14
|
k8s.io/kms v0.0.0-20240911031427-5781cdc762b1
|
||||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
|
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3
|
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -381,8 +381,8 @@ k8s.io/component-base v0.0.0-20240906193739-83f63c39727c h1:Gdhz79FA3l8bKFBM1eYC
|
||||||
k8s.io/component-base v0.0.0-20240906193739-83f63c39727c/go.mod h1:roKR20Tbxxh1HGxr9Fc1EzHkKJMH9Wfkau/U/EgjXys=
|
k8s.io/component-base v0.0.0-20240906193739-83f63c39727c/go.mod h1:roKR20Tbxxh1HGxr9Fc1EzHkKJMH9Wfkau/U/EgjXys=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kms v0.0.0-20240827234606-4aa59ca2ab14 h1:Ds3NwJtIZQZRU1kSOl83F/KPh+RBH41NqPhnhNL34ck=
|
k8s.io/kms v0.0.0-20240911031427-5781cdc762b1 h1:ei7efe802YLti9U57pZi3j7RvD5HRffTNDDWoEBAi90=
|
||||||
k8s.io/kms v0.0.0-20240827234606-4aa59ca2ab14/go.mod h1:ySeb/iDDdPkL9xksNTkh5F8l7/YykHrMnozHdH+gD0k=
|
k8s.io/kms v0.0.0-20240911031427-5781cdc762b1/go.mod h1:ySeb/iDDdPkL9xksNTkh5F8l7/YykHrMnozHdH+gD0k=
|
||||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=
|
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=
|
||||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA=
|
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA=
|
||||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,15 @@ var regexLib = ®ex{}
|
||||||
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{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
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) {
|
||||||
|
env, err := cel.NewEnv(
|
||||||
|
library.SemverLib(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
compiled, issues := env.Compile(expr)
|
||||||
|
|
||||||
|
if len(expectCompileErrs) > 0 {
|
||||||
|
missingCompileErrs := []string{}
|
||||||
|
matchedCompileErrs := sets.New[int]()
|
||||||
|
for _, expectedCompileErr := range expectCompileErrs {
|
||||||
|
compiledPattern, err := regexp.Compile(expectedCompileErr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to compile expected err regex: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
didMatch := false
|
||||||
|
|
||||||
|
for i, compileError := range issues.Errors() {
|
||||||
|
if compiledPattern.Match([]byte(compileError.Message)) {
|
||||||
|
didMatch = true
|
||||||
|
matchedCompileErrs.Insert(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !didMatch {
|
||||||
|
missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
|
||||||
|
} else if len(matchedCompileErrs) != len(issues.Errors()) {
|
||||||
|
unmatchedErrs := []cel.Error{}
|
||||||
|
for i, issue := range issues.Errors() {
|
||||||
|
if !matchedCompileErrs.Has(i) {
|
||||||
|
unmatchedErrs = append(unmatchedErrs, *issue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Empty(t, unmatchedErrs, "unexpected compilation errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Empty(t, missingCompileErrs, "expected compilation errors")
|
||||||
|
return
|
||||||
|
} else if len(issues.Errors()) > 0 {
|
||||||
|
for _, err := range issues.Errors() {
|
||||||
|
t.Errorf("unexpected compile error: %v", err)
|
||||||
|
}
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
prog, err := env.Program(compiled)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
res, _, err := prog.Eval(map[string]interface{}{})
|
||||||
|
if len(expectRuntimeErrPattern) > 0 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
|
||||||
|
} else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil {
|
||||||
|
t.Fatalf("failed to compile expected err regex: %v", regexErr)
|
||||||
|
} else if !matched {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
} else if expectResult != nil {
|
||||||
|
converted := res.Equal(expectResult).Value().(bool)
|
||||||
|
require.True(t, converted, "expectation not equal to output")
|
||||||
|
} else {
|
||||||
|
t.Fatal("expected result must not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSemver(t *testing.T) {
|
||||||
|
trueVal := types.Bool(true)
|
||||||
|
falseVal := types.Bool(false)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
expectValue ref.Val
|
||||||
|
expectedCompileErr []string
|
||||||
|
expectedRuntimeErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "parse",
|
||||||
|
expr: `semver("1.2.3")`,
|
||||||
|
expectValue: apiservercel.Semver{Version: semver.MustParse("1.2.3")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parseInvalidVersion",
|
||||||
|
expr: `semver("v1.0")`,
|
||||||
|
expectedRuntimeErr: "No Major.Minor.Patch elements found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isSemver",
|
||||||
|
expr: `isSemver("1.2.3-beta.1+build.1")`,
|
||||||
|
expectValue: trueVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isSemver_false",
|
||||||
|
expr: `isSemver("v1.0")`,
|
||||||
|
expectValue: falseVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "isSemver_noOverload",
|
||||||
|
expr: `isSemver([1, 2, 3])`,
|
||||||
|
expectedCompileErr: []string{"found no matching overload for 'isSemver' applied to.*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equality_reflexivity",
|
||||||
|
expr: `semver("1.2.3") == semver("1.2.3")`,
|
||||||
|
expectValue: trueVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inequality",
|
||||||
|
expr: `semver("1.2.3") == semver("1.0.0")`,
|
||||||
|
expectValue: falseVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "semver_less",
|
||||||
|
expr: `semver("1.0.0").isLessThan(semver("1.2.3"))`,
|
||||||
|
expectValue: trueVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "semver_less_false",
|
||||||
|
expr: `semver("1.0.0").isLessThan(semver("1.0.0"))`,
|
||||||
|
expectValue: falseVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "semver_greater",
|
||||||
|
expr: `semver("1.2.3").isGreaterThan(semver("1.0.0"))`,
|
||||||
|
expectValue: trueVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "semver_greater_false",
|
||||||
|
expr: `semver("1.0.0").isGreaterThan(semver("1.0.0"))`,
|
||||||
|
expectValue: falseVal,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compare_equal",
|
||||||
|
expr: `semver("1.2.3").compareTo(semver("1.2.3"))`,
|
||||||
|
expectValue: types.Int(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compare_less",
|
||||||
|
expr: `semver("1.0.0").compareTo(semver("1.2.3"))`,
|
||||||
|
expectValue: types.Int(-1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compare_greater",
|
||||||
|
expr: `semver("1.2.3").compareTo(semver("1.0.0"))`,
|
||||||
|
expectValue: types.Int(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "major",
|
||||||
|
expr: `semver("1.2.3").major()`,
|
||||||
|
expectValue: types.Int(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "minor",
|
||||||
|
expr: `semver("1.2.3").minor()`,
|
||||||
|
expectValue: types.Int(2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "patch",
|
||||||
|
expr: `semver("1.2.3").patch()`,
|
||||||
|
expectValue: types.Int(3),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
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/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].
|
||||||
|
//
|
||||||
|
// semver
|
||||||
|
//
|
||||||
|
// Converts a string to a semantic version or results in an error if the string is not a valid semantic version. Refer
|
||||||
|
// to semver.org documentation for information on accepted patterns.
|
||||||
|
//
|
||||||
|
// semver(<string>) <Semver>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// semver('1.0.0') // returns a Semver
|
||||||
|
// semver('0.1.0-alpha.1') // returns a Semver
|
||||||
|
// semver('200K') // error
|
||||||
|
// semver('Three') // error
|
||||||
|
// semver('Mi') // error
|
||||||
|
//
|
||||||
|
// isSemver
|
||||||
|
//
|
||||||
|
// Returns true if a string is a valid Semver. isSemver returns true if and
|
||||||
|
// only if semver does not result in error.
|
||||||
|
//
|
||||||
|
// isSemver( <string>) <bool>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// isSemver('1.0.0') // returns true
|
||||||
|
// isSemver('v1.0') // returns true (tolerant parsing)
|
||||||
|
// isSemver('hello') // returns false
|
||||||
|
//
|
||||||
|
// Conversion to Scalars:
|
||||||
|
//
|
||||||
|
// - major/minor/patch: return the major version number as int64.
|
||||||
|
//
|
||||||
|
// <Semver>.major() <int>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// semver("1.2.3").major() // returns 1
|
||||||
|
//
|
||||||
|
// Comparisons
|
||||||
|
//
|
||||||
|
// - isGreaterThan: Returns true if and only if the receiver is greater than the operand
|
||||||
|
//
|
||||||
|
// - isLessThan: Returns true if and only if the receiver is less than the operand
|
||||||
|
//
|
||||||
|
// - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// <Semver>.isLessThan(<semver>) <bool>
|
||||||
|
// <Semver>.isGreaterThan(<semver>) <bool>
|
||||||
|
// <Semver>.compareTo(<semver>) <int>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// semver("1.2.3").compareTo(semver("1.2.3")) // returns 0
|
||||||
|
// semver("1.2.3").compareTo(semver("2.0.0")) // returns -1
|
||||||
|
// semver("1.2.3").compareTo(semver("0.1.2")) // returns 1
|
||||||
|
|
||||||
|
func SemverLib() cel.EnvOption {
|
||||||
|
return cel.Lib(semverLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
var semverLib = &semverLibType{}
|
||||||
|
|
||||||
|
type semverLibType struct{}
|
||||||
|
|
||||||
|
func (*semverLibType) LibraryName() string {
|
||||||
|
return "kubernetes.Semver"
|
||||||
|
}
|
||||||
|
|
||||||
|
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}, 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{apiservercel.SemverType, apiservercel.SemverType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
|
||||||
|
},
|
||||||
|
"isLessThan": {
|
||||||
|
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{apiservercel.SemverType, apiservercel.SemverType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
|
||||||
|
},
|
||||||
|
"major": {
|
||||||
|
cel.MemberOverload("semver_major", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMajor)),
|
||||||
|
},
|
||||||
|
"minor": {
|
||||||
|
cel.MemberOverload("semver_minor", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverMinor)),
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
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...))
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*semverLibType) ProgramOptions() []cel.ProgramOption {
|
||||||
|
return []cel.ProgramOption{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSemver(arg ref.Val) ref.Val {
|
||||||
|
str, ok := arg.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using semver/v4 here is okay because this function isn't
|
||||||
|
// used to validate the Kubernetes API. In the CEL base library
|
||||||
|
// we would have to use the regular expression from
|
||||||
|
// pkg/apis/resource/structured/namedresources/validation/validation.go.
|
||||||
|
_, err := semver.Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
return types.Bool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Bool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToSemver(arg ref.Val) ref.Val {
|
||||||
|
str, ok := arg.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using semver/v4 here is okay because this function isn't
|
||||||
|
// used to validate the Kubernetes API. In the CEL base library
|
||||||
|
// we would have to use the regular expression from
|
||||||
|
// pkg/apis/resource/structured/namedresources/validation/validation.go
|
||||||
|
// first before parsing.
|
||||||
|
v, err := semver.Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
return types.WrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiservercel.Semver{Version: v}
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverMajor(arg ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
return types.Int(v.Major)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverMinor(arg ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
return types.Int(v.Minor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverPatch(arg ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
return types.Int(v.Patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverIsGreaterThan(arg ref.Val, other ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
v2, ok := other.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Bool(v.Compare(v2) == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverIsLessThan(arg ref.Val, other ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
v2, ok := other.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Bool(v.Compare(v2) == -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func semverCompareTo(arg ref.Val, other ref.Val) ref.Val {
|
||||||
|
v, ok := arg.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
v2, ok := other.Value().(semver.Version)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Int(v.Compare(v2))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
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 cel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SemverType = cel.ObjectType("kubernetes.Semver")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Semver provdes a CEL representation of a [semver.Version].
|
||||||
|
type Semver struct {
|
||||||
|
semver.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Semver) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
||||||
|
if reflect.TypeOf(v.Version).AssignableTo(typeDesc) {
|
||||||
|
return v.Version, nil
|
||||||
|
}
|
||||||
|
if reflect.TypeOf("").AssignableTo(typeDesc) {
|
||||||
|
return v.Version.String(), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("type conversion error from 'Semver' to '%v'", typeDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Semver) ConvertToType(typeVal ref.Type) ref.Val {
|
||||||
|
switch typeVal {
|
||||||
|
case SemverType:
|
||||||
|
return v
|
||||||
|
case types.TypeType:
|
||||||
|
return SemverType
|
||||||
|
default:
|
||||||
|
return types.NewErr("type conversion error from '%s' to '%s'", SemverType, typeVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Semver) Equal(other ref.Val) ref.Val {
|
||||||
|
otherDur, ok := other.(Semver)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(other)
|
||||||
|
}
|
||||||
|
return types.Bool(v.Version.EQ(otherDur.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Semver) Type() ref.Type {
|
||||||
|
return SemverType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Semver) Value() interface{} {
|
||||||
|
return v.Version
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue