mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 02:41:25 +00:00
composited type systems for CEL.
This commit is contained in:
parent
0270fc75d0
commit
9633cb8d7e
119
staging/src/k8s.io/apiserver/pkg/cel/composited.go
Normal file
119
staging/src/k8s.io/apiserver/pkg/cel/composited.go
Normal file
@ -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 {
|
||||||
|
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) {
|
||||||
|
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
staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go
Normal file
176
staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go
Normal file
@ -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"
|
||||||
|
"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 {
|
||||||
|
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
|
||||||
|
}
|
@ -360,6 +360,23 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
|||||||
if rt == nil {
|
if rt == nil {
|
||||||
return []cel.EnvOption{}, nil
|
return []cel.EnvOption{}, nil
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
|
if rt == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
var ta ref.TypeAdapter = types.DefaultTypeAdapter
|
var ta ref.TypeAdapter = types.DefaultTypeAdapter
|
||||||
tpa, ok := tp.(ref.TypeAdapter)
|
tpa, ok := tp.(ref.TypeAdapter)
|
||||||
if ok {
|
if ok {
|
||||||
@ -382,11 +399,7 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
|||||||
"type %s definition differs between CEL environment and rule", name)
|
"type %s definition differs between CEL environment and rule", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []cel.EnvOption{
|
return rtWithTypes, nil
|
||||||
cel.CustomTypeProvider(rtWithTypes),
|
|
||||||
cel.CustomTypeAdapter(rtWithTypes),
|
|
||||||
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not
|
// FindType attempts to resolve the typeName provided from the rule's rule-schema, or if not
|
||||||
|
Loading…
Reference in New Issue
Block a user