Allowing direct CEL reserved keyword usage in CRD (#126188)

* automatically escape reserved keywords for direct usage

* Add reserved keyword support in a ratcheting way, add tests.

---------

Co-authored-by: Wenxue Zhao <ballista01@outlook.com>

Kubernetes-commit: a48a92c72ec7d4e2a8da396309abff9360faae75
This commit is contained in:
Cici Huang 2024-07-23 15:45:20 -07:00 committed by Kubernetes Publisher
parent c90207143c
commit 92ee9330ce
5 changed files with 101 additions and 45 deletions

6
go.mod
View File

@ -44,9 +44,9 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.0.0-20240723194852-871340c2e998
k8s.io/api v0.0.0-20240724010313-f04ea0bc861d
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe
k8s.io/client-go v0.0.0-20240723200359-dcfcc90795cc
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440
k8s.io/klog/v2 v2.130.1
k8s.io/kms v0.0.0-20240707024556-6e3528fa4c33
@ -123,3 +123,5 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace k8s.io/api => k8s.io/api v0.0.0-20240724010313-a789efa287e8

8
go.sum
View File

@ -370,12 +370,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20240723194852-871340c2e998 h1:XvMrEqepRsNkn8Bl60PB5TO4ZEOgr70bYrpAedjvTV8=
k8s.io/api v0.0.0-20240723194852-871340c2e998/go.mod h1:ytlEzqC2wOTwYET71W7+J+k7O2V7vrDuzmNLBSpgT+k=
k8s.io/api v0.0.0-20240724010313-a789efa287e8 h1:TISAHWnfAdn420WpN+fEHG6snbLbfaCAp3kHDoAkxIc=
k8s.io/api v0.0.0-20240724010313-a789efa287e8/go.mod h1:ytlEzqC2wOTwYET71W7+J+k7O2V7vrDuzmNLBSpgT+k=
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe h1:V9MwpYUwbKlfLKVrhpVuKWiat/LBIhm1pGB9/xdHm5Q=
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.0.0-20240723200359-dcfcc90795cc h1:qe0SREEjfE5w3ANvrSURWv00J/ISlqa9Sa3FCBYKRlg=
k8s.io/client-go v0.0.0-20240723200359-dcfcc90795cc/go.mod h1:XfEsPNNFOR0wNkr3BtkPUN668l7Sx1W4ECSUolQ0mA4=
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b h1:NTLYx38CAu+VstHvPLosqB6uSQUtSM+3Mqz2D/C5JpE=
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b/go.mod h1:Y6CzOT21oLI4O66cjiV5oSSUgOL7gG/VCG9n8XI8OxU=
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440 h1:14X+5sRQRsul6tLxIKTP0/DotvWlMd9DFCgMqHP1hZY=
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440/go.mod h1:dj2Pl05aLcVMZi2NXcwv+M/WdUVPEkisFPjDze7rbSk=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=

View File

@ -267,7 +267,10 @@ func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, h
if len(declTypes) > 0 {
provider := apiservercel.NewDeclTypeProvider(declTypes...)
providerOpts, err := provider.EnvOptions(base.TypeProvider())
if compatVer.AtLeast(version.MajorMinor(1, 31)) {
provider.SetRecognizeKeywordAsFieldName(true)
}
providerOpts, err := provider.EnvOptions(base.CELTypeProvider())
if err != nil {
return nil, err
}

View File

@ -43,6 +43,15 @@ func TestBaseEnvironment(t *testing.T) {
},
})
// The escaping happens while construct declType hence we use escaped format here directly.
gadgetsType := apiservercel.NewObjectType("Gadget",
map[string]*apiservercel.DeclField{
"__namespace__": {
Name: "__namespace__",
Type: apiservercel.StringType,
},
})
cases := []struct {
name string
typeVersionCombinations []envTypeAndVersion
@ -251,6 +260,42 @@ func TestBaseEnvironment(t *testing.T) {
},
},
},
{
name: "recognizeKeywordAsFieldName disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 30), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"gadget.namespace == 'buzz'"},
activation: map[string]any{"gadget": map[string]any{"namespace": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{gadgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("gadget", cel.ObjectType("Gadget")),
},
},
},
},
{
name: "recognizeKeywordAsFieldName enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 31), NewExpressions},
{version.MajorMinor(1, 30), StoredExpressions},
},
validExpressions: []string{"gadget.namespace == 'buzz'"},
activation: map[string]any{"gadget": map[string]any{"namespace": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{gadgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("gadget", cel.ObjectType("Gadget")),
},
},
},
},
}
for _, tc := range cases {

View File

@ -27,7 +27,6 @@ import (
"github.com/google/cel-go/common/types/traits"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto"
"k8s.io/apimachinery/pkg/api/resource"
)
@ -349,9 +348,14 @@ func NewDeclTypeProvider(rootTypes ...*DeclType) *DeclTypeProvider {
// DeclTypeProvider extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
// type-system.
type DeclTypeProvider struct {
registeredTypes map[string]*DeclType
typeProvider ref.TypeProvider
typeAdapter ref.TypeAdapter
registeredTypes map[string]*DeclType
typeProvider types.Provider
typeAdapter types.Adapter
recognizeKeywordAsFieldName bool
}
func (rt *DeclTypeProvider) SetRecognizeKeywordAsFieldName(recognize bool) {
rt.recognizeKeywordAsFieldName = recognize
}
func (rt *DeclTypeProvider) EnumValue(enumName string) ref.Val {
@ -366,7 +370,7 @@ func (rt *DeclTypeProvider) FindIdent(identName string) (ref.Val, bool) {
// as well as a custom ref.TypeProvider.
//
// If the DeclTypeProvider value is nil, an empty []cel.EnvOption set is returned.
func (rt *DeclTypeProvider) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
func (rt *DeclTypeProvider) EnvOptions(tp types.Provider) ([]cel.EnvOption, error) {
if rt == nil {
return []cel.EnvOption{}, nil
}
@ -382,54 +386,52 @@ func (rt *DeclTypeProvider) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, er
// WithTypeProvider returns a new DeclTypeProvider that sets the given TypeProvider
// If the original DeclTypeProvider is nil, the returned DeclTypeProvider is still nil.
func (rt *DeclTypeProvider) WithTypeProvider(tp ref.TypeProvider) (*DeclTypeProvider, error) {
func (rt *DeclTypeProvider) WithTypeProvider(tp types.Provider) (*DeclTypeProvider, error) {
if rt == nil {
return nil, nil
}
var ta ref.TypeAdapter = types.DefaultTypeAdapter
tpa, ok := tp.(ref.TypeAdapter)
var ta types.Adapter = types.DefaultTypeAdapter
tpa, ok := tp.(types.Adapter)
if ok {
ta = tpa
}
rtWithTypes := &DeclTypeProvider{
typeProvider: tp,
typeAdapter: ta,
registeredTypes: rt.registeredTypes,
typeProvider: tp,
typeAdapter: ta,
registeredTypes: rt.registeredTypes,
recognizeKeywordAsFieldName: rt.recognizeKeywordAsFieldName,
}
for name, declType := range rt.registeredTypes {
tpType, found := tp.FindType(name)
expT, err := declType.ExprType()
if err != nil {
return nil, fmt.Errorf("fail to get cel type: %s", err)
}
if found && !proto.Equal(tpType, expT) {
tpType, found := tp.FindStructType(name)
// cast celType to types.type
expT := declType.CelType()
if found && !expT.IsExactType(tpType) {
return nil, fmt.Errorf(
"type %s definition differs between CEL environment and type provider", name)
}
}
return rtWithTypes, nil
}
// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not
// FindStructType attempts to resolve the typeName provided from the rule's rule-schema, or if not
// from the embedded ref.TypeProvider.
//
// FindType overrides the default type-finding behavior of the embedded TypeProvider.
// FindStructType overrides the default type-finding behavior of the embedded TypeProvider.
//
// Note, when the type name is based on the Open API Schema, the name will reflect the object path
// where the type definition appears.
func (rt *DeclTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
func (rt *DeclTypeProvider) FindStructType(typeName string) (*types.Type, bool) {
if rt == nil {
return nil, false
}
declType, found := rt.findDeclType(typeName)
if found {
expT, err := declType.ExprType()
if err != nil {
return expT, false
}
expT := declType.CelType()
return expT, found
}
return rt.typeProvider.FindType(typeName)
return rt.typeProvider.FindStructType(typeName)
}
// FindDeclType returns the CPT type description which can be mapped to a CEL type.
@ -440,37 +442,41 @@ func (rt *DeclTypeProvider) FindDeclType(typeName string) (*DeclType, bool) {
return rt.findDeclType(typeName)
}
// FindFieldType returns a field type given a type name and field name, if found.
// FindStructFieldNames returns the field names associated with the type, if the type
// is found.
func (rt *DeclTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) {
return []string{}, false
}
// FindStructFieldType returns a field type given a type name and field name, if found.
//
// Note, the type name for an Open API Schema type is likely to be its qualified object path.
// If, in the future an object instance rather than a type name were provided, the field
// resolution might more accurately reflect the expected type model. However, in this case
// concessions were made to align with the existing CEL interfaces.
func (rt *DeclTypeProvider) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
func (rt *DeclTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) {
st, found := rt.findDeclType(typeName)
if !found {
return rt.typeProvider.FindFieldType(typeName, fieldName)
return rt.typeProvider.FindStructFieldType(typeName, fieldName)
}
f, found := st.Fields[fieldName]
if rt.recognizeKeywordAsFieldName && !found && celReservedSymbols.Has(fieldName) {
f, found = st.Fields["__"+fieldName+"__"]
}
if found {
ft := f.Type
expT, err := ft.ExprType()
if err != nil {
return nil, false
}
return &ref.FieldType{
expT := ft.CelType()
return &types.FieldType{
Type: expT,
}, true
}
// This could be a dynamic map.
if st.IsMap() {
et := st.ElemType
expT, err := et.ExprType()
if err != nil {
return nil, false
}
return &ref.FieldType{
expT := et.CelType()
return &types.FieldType{
Type: expT,
}, true
}