From 9633cb8d7e11aef2a4c0dc3de39c40e51b771e84 Mon Sep 17 00:00:00 2001 From: Jiahui Feng Date: Thu, 2 Mar 2023 17:24:31 -0800 Subject: [PATCH] composited type systems for CEL. --- .../k8s.io/apiserver/pkg/cel/composited.go | 119 ++++++++++++ .../pkg/cel/openapi/compiling_test.go | 176 ++++++++++++++++++ staging/src/k8s.io/apiserver/pkg/cel/types.go | 23 ++- 3 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/composited.go create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/cel/composited.go b/staging/src/k8s.io/apiserver/pkg/cel/composited.go new file mode 100644 index 00000000000..9e5e634d0c3 --- /dev/null +++ b/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 { + 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/compiling_test.go new file mode 100644 index 00000000000..6fbf5b1292f --- /dev/null +++ b/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" + "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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/types.go b/staging/src/k8s.io/apiserver/pkg/cel/types.go index 13171ad2128..b2cc92d59eb 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/types.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/types.go @@ -360,6 +360,23 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) { if rt == 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 tpa, ok := tp.(ref.TypeAdapter) 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) } } - 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