Merge pull request #122886 from jiahuif-forks/feature/cel/mutating-library

[CEL Library] Unstructured Object Construction Support

Kubernetes-commit: 2363cdcc399cbf428210efb2c51575ddcad2b84a
This commit is contained in:
Kubernetes Publisher 2024-01-26 22:33:11 +01:00
commit 0dd0e74922
15 changed files with 1025 additions and 8 deletions

8
go.mod
View File

@ -42,10 +42,10 @@ require (
google.golang.org/protobuf v1.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70
k8s.io/api v0.0.0-20240124211858-f3648a53522e
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114
k8s.io/klog/v2 v2.120.1
k8s.io/kms v0.0.0-20231220174908-0e979309a09f
k8s.io/kube-openapi v0.0.0-20231113174909-778a5567bc1e
@ -125,9 +125,9 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70
k8s.io/api => k8s.io/api v0.0.0-20240124211858-f3648a53522e
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523
k8s.io/client-go => k8s.io/client-go v0.0.0-20240124011219-8092c71d3605
k8s.io/component-base => k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48
k8s.io/component-base => k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114
k8s.io/kms => k8s.io/kms v0.0.0-20231220174908-0e979309a09f
)

8
go.sum
View File

@ -385,14 +385,14 @@ 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-20240124211857-8b0ccc2e7c70 h1:RC1/ThLmvqAGg+Rf6TdfZ14G0AwXXxRxgQVXO1HtJYk=
k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA=
k8s.io/api v0.0.0-20240124211858-f3648a53522e h1:Lv52wennNKzlcDrBtANztawHC8xaTllHm51WUKIP0Ew=
k8s.io/api v0.0.0-20240124211858-f3648a53522e/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA=
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523 h1:1iJCbQAZv58v4zxd0ECIIMnyYlFsPWa2hmjqGEsv/5g=
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523/go.mod h1:Oh3ZrffM1/I8O/43oAA+aoOYgSregIXHxcWJB9ZRfQ8=
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605 h1:Dw3Ctw+SS3YmJTpaYP2nhIs4XagL4ctjKY0pHxN4RT8=
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605/go.mod h1:WuuT9L6+pj4rHmL2pb22xnOdtSvjiEcpB18g9Fuk0js=
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48 h1:3HvTUZ0ry5c0P15P+glBxBj+eh8Uv2ijNvjEORH+oOQ=
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48/go.mod h1:ANnr9YwsqK1XgjzXj9fGHEMDOp0QddDkKgQLLBPZ7Kg=
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114 h1:7t7EBTKztnJ9XSMAc6ogXdhhv/EgWCz0tIn3uiNfmjY=
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114/go.mod h1:fqdOK4GDgnyAlr6wC5fhFijh1aJ4WNcEQOcn7VaylqA=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.0.0-20231220174908-0e979309a09f h1:rfX7Jxf6fLy/4qh6tWv+hwn7z25oYr6yZkcVVSSZqnE=

View File

@ -0,0 +1,27 @@
/*
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 common
import (
"github.com/google/cel-go/common/types/traits"
)
// RootTypeReferenceName is the root reference that all type names should start with.
const RootTypeReferenceName = "Object"
// ObjectTraits is the bitmask that represents traits that an object should have.
const ObjectTraits = traits.ContainerType

View File

@ -0,0 +1,45 @@
/*
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 common
import (
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// TypeResolver resolves a type by a given name.
type TypeResolver interface {
// Resolve resolves the type by its name, starting with "Object" as its root.
// The type that the name refers to must be an object.
// This function returns false if the name does not refer to a known object type.
Resolve(name string) (TypeRef, bool)
}
// TypeRef refers an object type that can be looked up for its fields.
type TypeRef interface {
ref.Type
// CELType wraps the TypeRef to be a type that is understood by CEL.
CELType() *types.Type
// Field finds the field by the field name, or false if the field is not known.
// This function directly return a FieldType that is known to CEL to be more customizable.
Field(name string) (*types.FieldType, bool)
// Val creates an instance for the TypeRef, given its fields and their values.
Val(fields map[string]ref.Val) ref.Val
}

View File

@ -0,0 +1,119 @@
/*
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 common
import (
"fmt"
"reflect"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
)
// ObjectVal is the CEL Val for an object that is constructed via the object
// construction syntax.
type ObjectVal struct {
typeRef TypeRef
fields map[string]ref.Val
}
// NewObjectVal creates an ObjectVal by its TypeRef and its fields.
func NewObjectVal(typeRef TypeRef, fields map[string]ref.Val) *ObjectVal {
return &ObjectVal{
typeRef: typeRef,
fields: fields,
}
}
var _ ref.Val = (*ObjectVal)(nil)
var _ traits.Zeroer = (*ObjectVal)(nil)
// ConvertToNative converts the object to map[string]any.
// All nested lists are converted into []any native type.
//
// It returns an error if the target type is not map[string]any,
// or any recursive conversion fails.
func (v *ObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
var result map[string]any
if typeDesc != reflect.TypeOf(result) {
return nil, fmt.Errorf("unable to convert to %v", typeDesc)
}
result = make(map[string]any, len(v.fields))
for k, v := range v.fields {
result[k] = convertField(v)
}
return result, nil
}
// ConvertToType supports type conversions between CEL value types supported by the expression language.
func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case v.typeRef:
return v
case types.TypeType:
return v.typeRef.CELType()
}
return types.NewErr("unsupported conversion into %v", typeValue)
}
// Equal returns true if the `other` value has the same type and content as the implementing struct.
func (v *ObjectVal) Equal(other ref.Val) ref.Val {
if rhs, ok := other.(*ObjectVal); ok {
return types.Bool(reflect.DeepEqual(v.fields, rhs.fields))
}
return types.Bool(false)
}
// Type returns the TypeValue of the value.
func (v *ObjectVal) Type() ref.Type {
return v.typeRef.CELType()
}
// Value returns its value as a map[string]any.
func (v *ObjectVal) Value() any {
var result any
var object map[string]any
result, err := v.ConvertToNative(reflect.TypeOf(object))
if err != nil {
return types.WrapErr(err)
}
return result
}
// IsZeroValue indicates whether the object is the zero value for the type.
// For the ObjectVal, it is zero value if and only if the fields map is empty.
func (v *ObjectVal) IsZeroValue() bool {
return len(v.fields) == 0
}
// convertField converts a referred ref.Val to its expected type.
// For objects, the expected type is map[string]any
// For lists, the expected type is []any
// For anything else, it is converted via value.Value()
func convertField(value ref.Val) any {
// special handling for lists, where the elements are converted with Value() instead of ConvertToNative
// to allow them to become native value of any type.
if listOfVal, ok := value.Value().([]ref.Val); ok {
var result []any
for _, v := range listOfVal {
result = append(result, v.Value())
}
return result
}
return value.Value()
}

View File

@ -0,0 +1,65 @@
/*
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 common
import (
"reflect"
"testing"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
func TestOptional(t *testing.T) {
for _, tc := range []struct {
name string
fields map[string]ref.Val
expected map[string]any
}{
{
name: "present",
fields: map[string]ref.Val{
"zero": types.OptionalOf(types.IntZero),
},
expected: map[string]any{
"zero": int64(0),
},
},
{
name: "none",
fields: map[string]ref.Val{
"absent": types.OptionalNone,
},
expected: map[string]any{
// right now no way to differ from a plain null.
// we will need to filter out optional.none() before this conversion.
"absent": nil,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
v := &ObjectVal{
typeRef: nil, // safe in this test, otherwise put a mock
fields: tc.fields,
}
converted := v.Value()
if !reflect.DeepEqual(tc.expected, converted) {
t.Errorf("wrong result, expected %v but got %v", tc.expected, converted)
}
})
}
}

View File

@ -0,0 +1,51 @@
/*
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 mutation
import (
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/environment"
)
// mustCreateEnv creates the default env for testing, with given option.
// it fatally fails the test if the env fails to set up.
func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).
Extend(environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 30),
EnvOptions: envOptions,
})
if err != nil {
t.Fatalf("fail to create env set: %v", err)
}
env, err := envSet.Env(environment.StoredExpressions)
if err != nil {
t.Fatalf("fail to setup env: %v", env)
}
return env
}
// mustCreateEnvWithOptional creates the default env for testing, with given option,
// and set up the optional library with default configuration.
// it fatally fails the test if the env fails to set up.
func mustCreateEnvWithOptional(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
return mustCreateEnv(t, append(envOptions, cel.OptionalTypes())...)
}

View File

@ -0,0 +1,110 @@
/*
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 mutation
import (
"strings"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apiserver/pkg/cel/mutation/common"
)
// mockTypeResolver is a mock implementation of TypeResolver that
// allows the object to contain any field.
type mockTypeResolver struct {
}
// mockTypeRef is a mock implementation of TypeRef that
// contains any field.
type mockTypeRef struct {
objectType *types.Type
resolver common.TypeResolver
}
func newMockTypeRef(resolver common.TypeResolver, name string) *mockTypeRef {
objectType := types.NewObjectType(name, common.ObjectTraits)
return &mockTypeRef{
objectType: objectType,
resolver: resolver,
}
}
func (m *mockTypeRef) HasTrait(trait int) bool {
return common.ObjectTraits|trait != 0
}
func (m *mockTypeRef) TypeName() string {
return m.objectType.TypeName()
}
func (m *mockTypeRef) CELType() *types.Type {
return types.NewTypeTypeWithParam(m.objectType)
}
func (m *mockTypeRef) Field(name string) (*types.FieldType, bool) {
return &types.FieldType{
Type: types.DynType,
IsSet: func(target any) bool {
return true
},
GetFrom: func(target any) (any, error) {
return nil, nil
},
}, true
}
func (m *mockTypeRef) Val(fields map[string]ref.Val) ref.Val {
return common.NewObjectVal(m, fields)
}
func (m *mockTypeResolver) Resolve(name string) (common.TypeRef, bool) {
if strings.HasPrefix(name, common.RootTypeReferenceName) {
return newMockTypeRef(m, name), true
}
return nil, false
}
// mockTypeResolverForOptional behaves the same as mockTypeResolver
// except returning a mockTypeRefForOptional instead of mockTypeRef
type mockTypeResolverForOptional struct {
*mockTypeResolver
}
// mockTypeRefForOptional behaves the same as the underlying TypeRef
// except treating "nonExisting" field as non-existing.
// This is used for optional tests.
type mockTypeRefForOptional struct {
common.TypeRef
}
// Field returns a mock FieldType, or false if the field should not exist.
func (m *mockTypeRefForOptional) Field(name string) (*types.FieldType, bool) {
if name == "nonExisting" {
return nil, false
}
return m.TypeRef.Field(name)
}
func (m *mockTypeResolverForOptional) Resolve(name string) (common.TypeRef, bool) {
r, ok := m.mockTypeResolver.Resolve(name)
if ok {
return &mockTypeRefForOptional{TypeRef: r}, ok
}
return nil, false
}

View File

@ -0,0 +1,147 @@
/*
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 mutation
import (
"strings"
"testing"
celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apiserver/pkg/cel/mutation/common"
)
// TestCELOptional is an exploration test to demonstrate how CEL optional library
// behave for the use cases that the mutation library requires.
func TestCELOptional(t *testing.T) {
for _, tc := range []struct {
name string
expression string
expectedVal ref.Val
expectedCompileError string
}{
{
// question mark syntax still requires the field to exist in object construction
name: "construct non-existing field, compile error",
expression: `Object{
?nonExisting: optional.none()
}`,
expectedCompileError: `undefined field 'nonExisting'`,
},
{
// The root cause of the behavior above is that, has on an object (or Message in the Language Def),
// still require the field to be declared in the schema.
//
// Quoting from
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
//
// To test for the presence of a field, the boolean-valued macro has(e.f) can be used.
//
// 2. If e evaluates to a message and f is not a declared field for the message,
// has(e.f) raises a no_such_field error.
name: "has(Object{}), de-sugared, compile error",
expression: "has(Object{}.nonExisting)",
expectedCompileError: `undefined field 'nonExisting'`,
},
{
name: "construct existing field with none, empty object",
expression: `Object{
?existing: optional.none()
}`,
expectedVal: common.NewObjectVal(nil, map[string]ref.Val{
// "existing" field was not set.
}),
},
{
name: "object of zero value, ofNonZeroValue",
expression: `Object{?spec: optional.ofNonZeroValue(Object.spec{?replicas: Object{}.?replicas})}`,
expectedVal: common.NewObjectVal(nil, map[string]ref.Val{
// "existing" field was not set.
}),
},
{
name: "access non-existing field, return none",
expression: `Object{}.?nonExisting`,
expectedCompileError: `undefined field 'nonExisting'`,
},
{
name: "access existing field, return none",
expression: `Object{}.?existing`,
expectedVal: celtypes.OptionalNone,
},
{
name: "map non-existing field, return none",
expression: `{"foo": 1}[?"bar"]`,
expectedVal: celtypes.OptionalNone,
},
{
name: "map existing field, return actual value",
expression: `{"foo": 1}[?"foo"]`,
expectedVal: celtypes.OptionalOf(celtypes.Int(1)),
},
{
// Map has a different behavior than Object
//
// Quoting from
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
//
// To test for the presence of a field, the boolean-valued macro has(e.f) can be used.
//
// 1. If e evaluates to a map, then has(e.f) indicates whether the string f is
// a key in the map (note that f must syntactically be an identifier).
//
name: "has on a map, de-sugared, non-existing field, returns false",
// has marco supports only the dot access syntax.
expression: `has({"foo": 1}.bar)`,
expectedVal: celtypes.False,
},
{
name: "has on a map, de-sugared, existing field, returns true",
// has marco supports only the dot access syntax.
expression: `has({"foo": 1}.foo)`,
expectedVal: celtypes.True,
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := NewTypeProviderAndEnvOption(&mockTypeResolverForOptional{
mockTypeResolver: &mockTypeResolver{},
})
env := mustCreateEnvWithOptional(t, option)
ast, issues := env.Compile(tc.expression)
if issues != nil {
if tc.expectedCompileError == "" {
t.Fatalf("unexpected issues during compilation: %v", issues)
} else if !strings.Contains(issues.String(), tc.expectedCompileError) {
t.Fatalf("unexpected compile error, want to contain %q but got %v", tc.expectedCompileError, issues)
}
return
}
program, err := env.Program(ast)
if err != nil {
t.Fatalf("unexpected error while creating program: %v", err)
}
r, _, err := program.Eval(map[string]any{})
if err != nil {
t.Fatalf("unexpected error during evaluation: %v", err)
}
if equals := tc.expectedVal.Equal(r); equals.Value() != true {
t.Errorf("expected %v but got %v", tc.expectedVal, r)
}
})
}
}

View File

@ -0,0 +1,94 @@
/*
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 mutation
import (
"k8s.io/apiserver/pkg/cel/mutation/common"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// TypeProvider is a specialized CEL type provider that understands
// the Object type alias that is used to construct an Apply configuration for
// a mutation operation.
type TypeProvider struct {
typeResolver common.TypeResolver
underlyingTypeProvider types.Provider
}
var _ types.Provider = (*TypeProvider)(nil)
// EnumValue returns the numeric value of the given enum value name.
// This TypeProvider does not have special handling for EnumValue and thus directly delegate
// to its underlying type provider.
func (p *TypeProvider) EnumValue(enumName string) ref.Val {
return p.underlyingTypeProvider.EnumValue(enumName)
}
// FindIdent takes a qualified identifier name and returns a ref.ObjectVal if one exists.
// This TypeProvider does not have special handling for FindIdent and thus directly delegate
// to its underlying type provider.
func (p *TypeProvider) FindIdent(identName string) (ref.Val, bool) {
return p.underlyingTypeProvider.FindIdent(identName)
}
// FindStructType returns the Type give a qualified type name, by looking it up with
// the TypeResolver and translating it to CEL Type.
// If the type is not known to the TypeResolver, the lookup falls back to the underlying
// TypeProvider instead.
func (p *TypeProvider) FindStructType(structType string) (*types.Type, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.CELType(), true
}
return p.underlyingTypeProvider.FindStructType(structType)
}
// FindStructFieldType returns the field type for a checked type value.
// Returns false if the field could not be found.
func (p *TypeProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.Field(fieldName)
}
return p.underlyingTypeProvider.FindStructFieldType(structType, fieldName)
}
// NewValue creates a new type value from a qualified name and map of fields.
func (p *TypeProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.Val(fields)
}
return p.underlyingTypeProvider.NewValue(structType, fields)
}
// NewTypeProviderAndEnvOption creates the TypeProvider with a given TypeResolver,
// and also returns the CEL EnvOption to apply it to the env.
func NewTypeProviderAndEnvOption(resolver common.TypeResolver) (*TypeProvider, cel.EnvOption) {
tp := &TypeProvider{typeResolver: resolver}
var envOption cel.EnvOption = func(e *cel.Env) (*cel.Env, error) {
// wrap the existing type provider (acquired from the env)
// and set new type provider for the env.
tp.underlyingTypeProvider = e.CELTypeProvider()
typeProviderOption := cel.CustomTypeProvider(tp)
return typeProviderOption(e)
}
return tp, envOption
}

View File

@ -0,0 +1,66 @@
/*
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 mutation
import (
"reflect"
"testing"
)
func TestTypeProvider(t *testing.T) {
for _, tc := range []struct {
name string
expression string
expectedValue any
}{
{
name: "not an object",
expression: `2 * 31 * 1847`,
expectedValue: int64(114514), // type resolver should not interfere.
},
{
name: "empty",
expression: "Object{}",
expectedValue: map[string]any{},
},
{
name: "Object.spec",
expression: "Object{spec: Object.spec{replicas: 3}}",
expectedValue: map[string]any{"spec": map[string]any{"replicas": int64(3)}},
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := NewTypeProviderAndEnvOption(&mockTypeResolver{})
env := mustCreateEnv(t, option)
ast, issues := env.Compile(tc.expression)
if issues != nil {
t.Fatalf("unexpected issues during compilation: %v", issues)
}
program, err := env.Program(ast)
if err != nil {
t.Fatalf("unexpected error while creating program: %v", err)
}
r, _, err := program.Eval(map[string]any{})
if err != nil {
t.Fatalf("unexpected error during evaluation: %v", err)
}
if v := r.Value(); !reflect.DeepEqual(tc.expectedValue, v) {
t.Errorf("expected %v but got %v", tc.expectedValue, v)
}
})
}
}

View File

@ -0,0 +1,43 @@
/*
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 unstructured
import (
"fmt"
"github.com/google/cel-go/common/types"
)
// NewFieldType creates a field by its field name.
// This version of FieldType is unstructured and has DynType as its type.
func NewFieldType(name string) *types.FieldType {
return &types.FieldType{
// for unstructured, we do not check for its type,
// use DynType for all fields.
Type: types.DynType,
IsSet: func(target any) bool {
// for an unstructured object, we allow any field to be considered set.
return true
},
GetFrom: func(target any) (any, error) {
if m, ok := target.(map[string]any); ok {
return m[name], nil
}
return nil, fmt.Errorf("cannot get field %q", name)
},
}
}

View File

@ -0,0 +1,66 @@
/*
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 unstructured
import (
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apiserver/pkg/cel/mutation/common"
)
// TypeRef is the implementation of TypeRef for an unstructured object.
// This is especially usefully when the schema is not known or available.
type TypeRef struct {
celObjectType *types.Type
celTypeType *types.Type
}
func (r *TypeRef) HasTrait(trait int) bool {
return common.ObjectTraits|trait != 0
}
// TypeName returns the name of this TypeRef.
func (r *TypeRef) TypeName() string {
return r.celObjectType.TypeName()
}
// Val returns an instance given the fields.
func (r *TypeRef) Val(fields map[string]ref.Val) ref.Val {
return common.NewObjectVal(r, fields)
}
// CELType returns the type. The returned type is of TypeType type.
func (r *TypeRef) CELType() *types.Type {
return r.celTypeType
}
// Field looks up the field by name.
// This is the unstructured version that allows any name as the field name.
// The returned field is of DynType type.
func (r *TypeRef) Field(name string) (*types.FieldType, bool) {
return NewFieldType(name), true
}
// NewTypeRef creates a TypeRef by the given field name.
func NewTypeRef(name string) *TypeRef {
objectType := types.NewObjectType(name, common.ObjectTraits)
return &TypeRef{
celObjectType: objectType,
celTypeType: types.NewTypeTypeWithParam(objectType),
}
}

View File

@ -0,0 +1,39 @@
/*
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 unstructured
import (
"strings"
"k8s.io/apiserver/pkg/cel/mutation/common"
)
const object = common.RootTypeReferenceName
type TypeResolver struct {
}
// Resolve resolves the TypeRef for the given type name
// that starts with "Object".
// This is the unstructured version, which means the
// returned TypeRef does not refer to the schema.
func (r *TypeResolver) Resolve(name string) (common.TypeRef, bool) {
if !strings.HasPrefix(name, object) {
return nil, false
}
return NewTypeRef(name), true
}

View File

@ -0,0 +1,145 @@
/*
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 unstructured
import (
"reflect"
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/mutation"
)
func TestTypeProvider(t *testing.T) {
for _, tc := range []struct {
name string
expression string
expectedValue any
}{
{
name: "not an object",
expression: `string(114514)`,
expectedValue: "114514",
},
{
name: "empty",
expression: "Object{}",
expectedValue: map[string]any{},
},
{
name: "Object.spec",
expression: "Object{spec: Object.spec{replicas: 3}}",
expectedValue: map[string]any{
"spec": map[string]any{
// an integer maps to int64
"replicas": int64(3),
},
},
},
{
// list literal does not require new path code of the type provider
// comparing to the object literal.
// This test case serves as a note of "supported syntax"
name: "Object.spec.template.containers",
expression: `Object{
spec: Object.spec{
template: Object.spec.template{
containers: [
Object.spec.template.containers.item{
name: "nginx",
image: "nginx",
args: ["-g"]
}
]
}
}
}`,
expectedValue: map[string]any{
"spec": map[string]any{
"template": map[string]any{
"containers": []any{
map[string]any{
"name": "nginx",
"image": "nginx",
"args": []any{"-g"},
},
},
},
},
},
},
{
name: "list of ints",
expression: `Object{
intList: [1, 2, 3]
}`,
expectedValue: map[string]any{
"intList": []any{int64(1), int64(2), int64(3)},
},
},
{
name: "field access",
expression: `Object{
intList: [1, 2, 3]
}.intList.sum()`,
expectedValue: int64(6),
},
{
name: "equality check",
expression: "Object{spec: Object.spec{replicas: 3}} == Object{spec: Object.spec{replicas: 1 + 2}}",
expectedValue: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := mutation.NewTypeProviderAndEnvOption(&TypeResolver{})
env := mustCreateEnv(t, option)
ast, issues := env.Compile(tc.expression)
if issues != nil {
t.Fatalf("unexpected issues during compilation: %v", issues)
}
program, err := env.Program(ast)
if err != nil {
t.Fatalf("unexpected error while creating program: %v", err)
}
r, _, err := program.Eval(map[string]any{})
if err != nil {
t.Fatalf("unexpected error during evaluation: %v", err)
}
if v := r.Value(); !reflect.DeepEqual(v, tc.expectedValue) {
t.Errorf("expected %v but got %v", tc.expectedValue, v)
}
})
}
}
func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).
Extend(environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 30),
EnvOptions: envOptions,
})
if err != nil {
t.Fatalf("fail to create env set: %v", err)
}
env, err := envSet.Env(environment.StoredExpressions)
if err != nil {
t.Fatalf("fail to setup env: %v", env)
}
return env
}