Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

composited type systems for CEL. #116267

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 119 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/cel/composited.go
@@ -0,0 +1,119 @@
/*
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 cel

import (
"github.com/google/cel-go/common/types/ref"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

var _ ref.TypeProvider = (*CompositedTypeProvider)(nil)
var _ ref.TypeAdapter = (*CompositedTypeAdapter)(nil)

// CompositedTypeProvider is the provider that tries each of the underlying
// providers in order, and returns result of the first successful attempt.
type CompositedTypeProvider struct {
// Providers contains the underlying type providers.
// If Providers is empty, the CompositedTypeProvider becomes no-op provider.
Providers []ref.TypeProvider
}

// EnumValue finds out the numeric value of the given enum name.
// The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) EnumValue(enumName string) ref.Val {
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
for _, p := range c.Providers {
val := p.EnumValue(enumName)
if val != nil {
return val
}
}
return nil
}

// FindIdent takes a qualified identifier name and returns a Value if one
// exists. The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) FindIdent(identName string) (ref.Val, bool) {
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
for _, p := range c.Providers {
val, ok := p.FindIdent(identName)
if ok {
return val, ok
}
}
return nil, false
}

// FindType finds the Type given a qualified type name, or return false
// if none of the providers finds the type.
// If any of the providers find the type, the first provider that returns true
// will be the result.
func (c *CompositedTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
for _, p := range c.Providers {
typ, ok := p.FindType(typeName)
if ok {
return typ, ok
}
}
return nil, false
}

// FindFieldType returns the field type for a checked type value. Returns
// false if none of the providers can find the type.
// If multiple providers can find the field, the result is taken from
// the first that does.
func (c *CompositedTypeProvider) FindFieldType(messageType string, fieldName string) (*ref.FieldType, bool) {
for _, p := range c.Providers {
ft, ok := p.FindFieldType(messageType, fieldName)
if ok {
return ft, ok
}
}
return nil, false
}

// NewValue creates a new type value from a qualified name and map of field
// name to value.
// If multiple providers can create the new type, the first that returns
// non-nil will decide the result.
func (c *CompositedTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
for _, p := range c.Providers {
v := p.NewValue(typeName, fields)
if v != nil {
return v
}
}
return nil
}

// CompositedTypeAdapter is the adapter that tries each of the underlying
// type adapter in order until the first successfully conversion.
type CompositedTypeAdapter struct {
// Adapters contains underlying type adapters.
// If Adapters is empty, the CompositedTypeAdapter becomes a no-op adapter.
Adapters []ref.TypeAdapter
}

// NativeToValue takes the value and convert it into a ref.Val
// The result comes from the first TypeAdapter that returns non-nil.
func (c *CompositedTypeAdapter) NativeToValue(value interface{}) ref.Val {
for _, a := range c.Adapters {
v := a.NativeToValue(value)
if v != nil {
return v
}
}
return nil
}
176 changes: 176 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go
@@ -0,0 +1,176 @@
/*
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 openapi

import (
"testing"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
"github.com/google/cel-go/interpreter"

apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/kube-openapi/pkg/validation/spec"
)

func TestMultipleTypes(t *testing.T) {
env, err := buildTestEnv()
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
expression string
expectCompileError bool
expectEvalResult bool
}{
{
expression: "foo.foo == bar.bar",
expectEvalResult: true,
},
{
expression: "foo.bar == 'value'",
expectCompileError: true,
},
{
expression: "foo.foo == 'value'",
expectEvalResult: true,
},
{
expression: "bar.bar == 'value'",
expectEvalResult: true,
},
{
expression: "foo.common + bar.common <= 2",
expectEvalResult: false, // 3 > 2
},
{
expression: "foo.confusion == bar.confusion",
expectCompileError: true,
},
} {
t.Run(tc.expression, func(t *testing.T) {
ast, issues := env.Compile(tc.expression)
if issues != nil {
if tc.expectCompileError {
return
}
t.Fatalf("compile error: %v", issues)
}
if issues != nil {
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
t.Fatal(issues)
}
p, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
ret, _, err := p.Eval(&simpleActivation{
foo: map[string]any{"foo": "value", "common": 1, "confusion": "114514"},
bar: map[string]any{"bar": "value", "common": 2, "confusion": 114514},
})
if err != nil {
t.Fatal(err)
}
if ret.Type() != types.BoolType {
t.Errorf("bad result type: %v", ret.Type())
}
if res := ret.Value().(bool); tc.expectEvalResult != res {
t.Errorf("expectEvalResult expression evaluates to %v, got %v", tc.expectEvalResult, res)
}
})
}

}

// buildTestEnv sets up an environment that contains two variables, "foo" and
// "bar".
// foo is an object with a string field "foo", an integer field "common", and a string field "confusion"
// bar is an object with a string field "bar", an integer field "common", and an integer field "confusion"
func buildTestEnv() (*cel.Env, error) {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
env, err := cel.NewEnv(opts...)
if err != nil {
return nil, err
}
reg := apiservercel.NewRegistry(env)

declType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true)
fooRT, err := apiservercel.NewRuleTypes("fooType", declType, reg)
if err != nil {
return nil, err
}
fooRT, err = fooRT.WithTypeProvider(env.TypeProvider())
if err != nil {
return nil, err
}
fooType, _ := fooRT.FindDeclType("fooType")

declType = common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true)
barRT, err := apiservercel.NewRuleTypes("barType", declType, reg)
if err != nil {
return nil, err
}
barRT, err = barRT.WithTypeProvider(env.TypeProvider())
if err != nil {
return nil, err
}
barType, _ := barRT.FindDeclType("barType")

opts = append(opts, cel.CustomTypeProvider(&apiservercel.CompositedTypeProvider{Providers: []ref.TypeProvider{fooRT, barRT}}))
opts = append(opts, cel.CustomTypeAdapter(&apiservercel.CompositedTypeAdapter{Adapters: []ref.TypeAdapter{fooRT, barRT}}))
opts = append(opts, cel.Variable("foo", fooType.CelType()))
opts = append(opts, cel.Variable("bar", barType.CelType()))
return env.Extend(opts...)
}

func simpleMapSchema(fieldName string, confusionSchema *spec.Schema) common.Schema {
return &Schema{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
fieldName: *spec.StringProperty(),
"common": *spec.Int64Property(),
"confusion": *confusionSchema,
},
},
}}
}

type simpleActivation struct {
foo any
bar any
}

func (a *simpleActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case "foo":
return a.foo, true
case "bar":
return a.bar, true
default:
return nil, false
}
}

func (a *simpleActivation) Parent() interpreter.Activation {
return nil
}
23 changes: 18 additions & 5 deletions staging/src/k8s.io/apiserver/pkg/cel/types.go
Expand Up @@ -360,6 +360,23 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
if rt == nil {
return []cel.EnvOption{}, nil
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
}
rtWithTypes, err := rt.WithTypeProvider(tp)
if err != nil {
return nil, err
}
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
}, nil
}

// WithTypeProvider returns a new RuleTypes that sets the given TypeProvider
// If the original RuleTypes is nil, the returned RuleTypes is still nil.
func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
jiahuif marked this conversation as resolved.
Show resolved Hide resolved
if rt == nil {
return nil, nil
}
var ta ref.TypeAdapter = types.DefaultTypeAdapter
tpa, ok := tp.(ref.TypeAdapter)
if ok {
Expand All @@ -382,11 +399,7 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
"type %s definition differs between CEL environment and rule", name)
}
}
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
}, nil
return rtWithTypes, nil
}

// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not
Expand Down