Clean up Object initialization support

The initial work of this had been merged before
this PR but was not yet in use. This simplifies
the implementation and adds some basic type
sanity checking.

Co-authored-by: Jiahui Feng <jhf@google.com>
This commit is contained in:
Joe Betz 2024-10-25 13:22:06 -04:00
parent 910c2e2dad
commit 9ee1ea9d37
20 changed files with 1217 additions and 1055 deletions

View File

@ -0,0 +1,127 @@
/*
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/cel"
"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.
// This function returns false if the name does not refer to a known object type.
Resolve(name string) (ResolvedType, bool)
}
// ResolvedType refers an object type that can be looked up for its fields.
type ResolvedType interface {
ref.Type
Type() *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)
// FieldNames returns the field names associated with the type, if the type
// is found.
FieldNames() ([]string, bool)
// Val creates an instance for the ResolvedType, given its fields and their values.
Val(fields map[string]ref.Val) ref.Val
}
// ResolverTypeProvider delegates type resolution first to the TypeResolver and then
// to the underlying types.Provider for types not resolved by the TypeResolver.
type ResolverTypeProvider struct {
typeResolver TypeResolver
underlyingTypeProvider types.Provider
}
var _ types.Provider = (*ResolverTypeProvider)(nil)
// FindStructType returns the Type give a qualified type name, by looking it up with
// the DynamicTypeResolver and translating it to CEL Type.
// If the type is not known to the DynamicTypeResolver, the lookup falls back to the underlying
// ResolverTypeProvider instead.
func (p *ResolverTypeProvider) FindStructType(structType string) (*types.Type, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return types.NewTypeTypeWithParam(t.Type()), true
}
return p.underlyingTypeProvider.FindStructType(structType)
}
// FindStructFieldNames returns the field names associated with the type, if the type
// is found.
func (p *ResolverTypeProvider) FindStructFieldNames(structType string) ([]string, bool) {
t, ok := p.typeResolver.Resolve(structType)
if ok {
return t.FieldNames()
}
return p.underlyingTypeProvider.FindStructFieldNames(structType)
}
// FindStructFieldType returns the field type for a checked type value.
// Returns false if the field could not be found.
func (p *ResolverTypeProvider) 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 *ResolverTypeProvider) 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)
}
func (p *ResolverTypeProvider) EnumValue(enumName string) ref.Val {
return p.underlyingTypeProvider.EnumValue(enumName)
}
func (p *ResolverTypeProvider) FindIdent(identName string) (ref.Val, bool) {
return p.underlyingTypeProvider.FindIdent(identName)
}
// ResolverEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver,
// and also returns the CEL ResolverEnvOption to apply it to the env.
func ResolverEnvOption(resolver TypeResolver) cel.EnvOption {
_, envOpt := NewResolverTypeProviderAndEnvOption(resolver)
return envOpt
}
// NewResolverTypeProviderAndEnvOption creates the ResolverTypeProvider with a given DynamicTypeResolver,
// and also returns the CEL ResolverEnvOption to apply it to the env.
func NewResolverTypeProviderAndEnvOption(resolver TypeResolver) (*ResolverTypeProvider, cel.EnvOption) {
tp := &ResolverTypeProvider{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,170 @@
/*
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/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"reflect"
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
func TestTypeProvider(t *testing.T) {
for _, tc := range []struct {
name string
expression string
expectedValue any
expectCompileError string
}{
{
name: "not an object",
expression: `2 * 31 * 1847`,
expectedValue: int64(114514), // type resolver should not interfere.
},
{
name: "empty",
expression: "Test{}",
expectedValue: map[string]any{},
},
{
name: "simple",
expression: "Test{x: 1}",
expectedValue: map[string]any{"x": int64(1)},
},
{
name: "invalid type",
expression: "Objectfoo{}",
expectCompileError: "undeclared reference to 'Objectfoo'",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := NewResolverTypeProviderAndEnvOption(&mockTypeResolver{})
env := mustCreateEnv(t, option)
ast, issues := env.Compile(tc.expression)
if len(tc.expectCompileError) > 0 {
if issues == nil {
t.Fatalf("expected error %v but got no error", tc.expectCompileError)
}
if !strings.Contains(issues.String(), tc.expectCompileError) {
t.Fatalf("expected error %v but got %v", tc.expectCompileError, issues.String())
}
return
}
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)
}
})
}
}
// mockTypeResolver is a mock implementation of DynamicTypeResolver that
// allows the object to contain any field.
type mockTypeResolver struct {
}
func (m *mockTypeResolver) Resolve(name string) (ResolvedType, bool) {
if name == "Test" {
return newMockResolvedType(m, name), true
}
return nil, false
}
// mockResolvedType is a mock implementation of ResolvedType that
// contains any field.
type mockResolvedType struct {
objectType *types.Type
resolver TypeResolver
}
func newMockResolvedType(resolver TypeResolver, name string) *mockResolvedType {
objectType := types.NewObjectType(name)
return &mockResolvedType{
objectType: objectType,
resolver: resolver,
}
}
func (m *mockResolvedType) HasTrait(trait int) bool {
return m.objectType.HasTrait(trait)
}
func (m *mockResolvedType) TypeName() string {
return m.objectType.TypeName()
}
func (m *mockResolvedType) Type() *types.Type {
return m.objectType
}
func (m *mockResolvedType) TypeType() *types.Type {
return types.NewTypeTypeWithParam(m.objectType)
}
func (m *mockResolvedType) 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 *mockResolvedType) FieldNames() ([]string, bool) {
return nil, true
}
func (m *mockResolvedType) Val(fields map[string]ref.Val) ref.Val {
return dynamic.NewObjectVal(m.objectType, fields)
}
// 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(), true).
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
}

View File

@ -1,27 +0,0 @@
/*
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

@ -1,45 +0,0 @@
/*
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

@ -1,140 +0,0 @@
/*
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 {
converted, err := convertField(v)
if err != nil {
return nil, fmt.Errorf("fail to convert field %q: %w", k, err)
}
result[k] = converted
}
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 maps, the expected type is map[string]any
// For anything else, it is converted via value.Value()
//
// It will return an error if the request type is a map but the key
// is not a string.
func convertField(value ref.Val) (any, error) {
// 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, nil
}
// unstructured maps, as seen in annotations
// map keys must be strings
if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok {
result := make(map[string]any)
for k, v := range mapOfVal {
stringKey, ok := k.Value().(string)
if !ok {
return nil, fmt.Errorf("map key %q is of type %t, not string", k, k)
}
result[stringKey] = v.Value()
}
return result, nil
}
return value.Value(), nil
}

View File

@ -1,65 +0,0 @@
/*
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,249 @@
/*
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 dynamic
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"google.golang.org/protobuf/types/known/structpb"
)
// ObjectType is the implementation of the Object type for use when compiling
// CEL expressions without schema information about the object.
// This is to provide CEL expressions with access to Object{} types constructors.
type ObjectType struct {
objectType *types.Type
}
func (o *ObjectType) HasTrait(trait int) bool {
return o.objectType.HasTrait(trait)
}
// TypeName returns the name of this ObjectType.
func (o *ObjectType) TypeName() string {
return o.objectType.TypeName()
}
// Val returns an instance given the fields.
func (o *ObjectType) Val(fields map[string]ref.Val) ref.Val {
return NewObjectVal(o.objectType, fields)
}
func (o *ObjectType) Type() *types.Type {
return o.objectType
}
// 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 (o *ObjectType) Field(name string) (*types.FieldType, bool) {
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 {
if m, ok := target.(map[string]any); ok {
_, isSet := m[name]
return isSet
}
return false
},
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)
},
}, true
}
func (o *ObjectType) FieldNames() ([]string, bool) {
return nil, true // Field names are not known for dynamic types. All field names are allowed.
}
// NewObjectType creates a ObjectType by the given field name.
func NewObjectType(name string) *ObjectType {
return &ObjectType{
objectType: types.NewObjectType(name),
}
}
// ObjectVal is the CEL Val for an object that is constructed via the Object{} in
// CEL expressions without schema information about the object.
type ObjectVal struct {
objectType *types.Type
fields map[string]ref.Val
}
// NewObjectVal creates an ObjectVal by its ResolvedType and its fields.
func NewObjectVal(objectType *types.Type, fields map[string]ref.Val) *ObjectVal {
return &ObjectVal{
objectType: objectType,
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) {
result := make(map[string]any, len(v.fields))
for k, v := range v.fields {
converted, err := convertField(v)
if err != nil {
return nil, fmt.Errorf("fail to convert field %q: %w", k, err)
}
result[k] = converted
}
if typeDesc == reflect.TypeOf(result) {
return result, nil
}
// CEL's builtin data literal values all support conversion to structpb.Value, which
// can then be serialized to JSON. This is convenient for CEL expressions that return
// an arbitrary JSON value, such as our MutatingAdmissionPolicy JSON Patch valueExpression
// field, so we support the conversion here, for Object data literals, as well.
if typeDesc == reflect.TypeOf(&structpb.Value{}) {
return structpb.NewStruct(result)
}
return nil, fmt.Errorf("unable to convert to %v", typeDesc)
}
// ConvertToType supports type conversions between CEL value types supported by the expression language.
func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val {
if v.objectType.TypeName() == typeValue.TypeName() {
return v
}
if typeValue == types.TypeType {
return types.NewTypeTypeWithParam(v.objectType)
}
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 {
if v.objectType.Equal(rhs.objectType) != types.True {
return types.False
}
return types.Bool(reflect.DeepEqual(v.fields, rhs.fields))
}
return types.False
}
// Type returns the TypeValue of the value.
func (v *ObjectVal) Type() ref.Type {
return types.NewObjectType(v.objectType.TypeName())
}
// 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
}
// CheckTypeNamesMatchFieldPathNames transitively checks the CEL object type names of this ObjectVal. Returns all
// found type name mismatch errors.
// Children ObjectVal types under <field> or this ObjectVal
// must have type names of the form "<ObjectVal.TypeName>.<field>", children of that type must have type names of the
// form "<ObjectVal.TypeName>.<field>.<field>" and so on.
// Intermediate maps and lists are unnamed and ignored.
func (v *ObjectVal) CheckTypeNamesMatchFieldPathNames() error {
return errors.Join(typeCheck(v, []string{v.Type().TypeName()})...)
}
func typeCheck(v ref.Val, typeNamePath []string) []error {
var errs []error
if ov, ok := v.(*ObjectVal); ok {
tn := ov.objectType.TypeName()
if strings.Join(typeNamePath, ".") != tn {
errs = append(errs, fmt.Errorf("unexpected type name %q, expected %q, which matches field name path from root Object type", tn, strings.Join(typeNamePath, ".")))
}
for k, f := range ov.fields {
errs = append(errs, typeCheck(f, append(typeNamePath, k))...)
}
}
value := v.Value()
if listOfVal, ok := value.([]ref.Val); ok {
for _, v := range listOfVal {
errs = append(errs, typeCheck(v, typeNamePath)...)
}
}
if mapOfVal, ok := value.(map[ref.Val]ref.Val); ok {
for _, v := range mapOfVal {
errs = append(errs, typeCheck(v, typeNamePath)...)
}
}
return errs
}
// 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 maps, the expected type is map[string]any
// For anything else, it is converted via value.Value()
//
// It will return an error if the request type is a map but the key
// is not a string.
func convertField(value ref.Val) (any, error) {
// 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, nil
}
// unstructured maps, as seen in annotations
// map keys must be strings
if mapOfVal, ok := value.Value().(map[ref.Val]ref.Val); ok {
result := make(map[string]any)
for k, v := range mapOfVal {
stringKey, ok := k.Value().(string)
if !ok {
return nil, fmt.Errorf("map key %q is of type %t, not string", k, k)
}
result[stringKey] = v.Value()
}
return result, nil
}
return value.Value(), nil
}

View File

@ -0,0 +1,166 @@
/*
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 dynamic
import (
"reflect"
"strings"
"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{
objectType: 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)
}
})
}
}
func TestCheckTypeNamesMatchFieldPathNames(t *testing.T) {
for _, tc := range []struct {
name string
obj *ObjectVal
expectError string
}{
{
name: "valid",
obj: &ObjectVal{
objectType: types.NewObjectType("Object"),
fields: map[string]ref.Val{
"spec": &ObjectVal{
objectType: types.NewObjectType("Object.spec"),
fields: map[string]ref.Val{
"replicas": types.Int(100),
"m": types.NewRefValMap(nil, map[ref.Val]ref.Val{
types.String("k1"): &ObjectVal{
objectType: types.NewObjectType("Object.spec.m"),
},
}),
"l": types.NewRefValList(nil, []ref.Val{
&ObjectVal{
objectType: types.NewObjectType("Object.spec.l"),
},
}),
},
},
},
},
},
{
name: "invalid struct field",
obj: &ObjectVal{
objectType: types.NewObjectType("Object"),
fields: map[string]ref.Val{"invalid": &ObjectVal{
objectType: types.NewObjectType("Object.spec"),
fields: map[string]ref.Val{"replicas": types.Int(100)},
}},
},
expectError: "unexpected type name \"Object.spec\", expected \"Object.invalid\"",
},
{
name: "invalid map field",
obj: &ObjectVal{
objectType: types.NewObjectType("Object"),
fields: map[string]ref.Val{
"spec": &ObjectVal{
objectType: types.NewObjectType("Object.spec"),
fields: map[string]ref.Val{
"replicas": types.Int(100),
"m": types.NewRefValMap(nil, map[ref.Val]ref.Val{
types.String("k1"): &ObjectVal{
objectType: types.NewObjectType("Object.spec.invalid"),
},
}),
},
},
},
},
expectError: "unexpected type name \"Object.spec.invalid\", expected \"Object.spec.m\"",
},
{
name: "invalid list field",
obj: &ObjectVal{
objectType: types.NewObjectType("Object"),
fields: map[string]ref.Val{
"spec": &ObjectVal{
objectType: types.NewObjectType("Object.spec"),
fields: map[string]ref.Val{
"replicas": types.Int(100),
"l": types.NewRefValList(nil, []ref.Val{
&ObjectVal{
objectType: types.NewObjectType("Object.spec.invalid"),
},
}),
},
},
},
},
expectError: "unexpected type name \"Object.spec.invalid\", expected \"Object.spec.l\"",
},
} {
t.Run(tc.name, func(t *testing.T) {
err := tc.obj.CheckTypeNamesMatchFieldPathNames()
if tc.expectError == "" {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
} else {
if err == nil {
t.Errorf("expected error")
}
if !strings.Contains(err.Error(), tc.expectError) {
t.Errorf("expected error to contain %v, got %v", tc.expectError, err)
}
}
})
}
}

View File

@ -1,51 +0,0 @@
/*
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(), true).
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,178 @@
/*
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 (
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"reflect"
)
var jsonPatchType = types.NewObjectType(JSONPatchTypeName)
var (
jsonPatchOp = "op"
jsonPatchPath = "path"
jsonPatchFrom = "from"
jsonPatchValue = "value"
)
// JSONPatchType and JSONPatchVal are defined entirely from scratch here because JSONPatchVal
// has a dynamic 'value' field which can not be defined with an OpenAPI schema,
// preventing us from using DeclType and UnstructuredToVal.
// JSONPatchType provides a CEL type for "JSONPatch" operations.
type JSONPatchType struct{}
func (r *JSONPatchType) HasTrait(trait int) bool {
return jsonPatchType.HasTrait(trait)
}
// TypeName returns the name of this ObjectType.
func (r *JSONPatchType) TypeName() string {
return jsonPatchType.TypeName()
}
// Val returns an instance given the fields.
func (r *JSONPatchType) Val(fields map[string]ref.Val) ref.Val {
result := &JSONPatchVal{}
for name, value := range fields {
switch name {
case jsonPatchOp:
if s, ok := value.Value().(string); ok {
result.Op = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'op' field", value.Value())
}
case jsonPatchPath:
if s, ok := value.Value().(string); ok {
result.Path = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'path' field", value.Value())
}
case jsonPatchFrom:
if s, ok := value.Value().(string); ok {
result.From = s
} else {
return types.NewErr("unexpected type %T for JSONPatchType 'from' field", value.Value())
}
case jsonPatchValue:
result.Val = value
default:
return types.NewErr("unexpected JSONPatchType field: %s", name)
}
}
return result
}
func (r *JSONPatchType) Type() *types.Type {
return jsonPatchType
}
func (r *JSONPatchType) Field(name string) (*types.FieldType, bool) {
var fieldType *types.Type
switch name {
case jsonPatchOp, jsonPatchFrom, jsonPatchPath:
fieldType = cel.StringType
case jsonPatchValue:
fieldType = types.DynType
}
return &types.FieldType{
Type: fieldType,
}, true
}
func (r *JSONPatchType) FieldNames() ([]string, bool) {
return []string{jsonPatchOp, jsonPatchFrom, jsonPatchPath, jsonPatchValue}, true
}
// JSONPatchVal is the ref.Val for a JSONPatch.
type JSONPatchVal struct {
Op, From, Path string
Val ref.Val
}
func (p *JSONPatchVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
if typeDesc == reflect.TypeOf(&JSONPatchVal{}) {
return p, nil
}
return nil, fmt.Errorf("cannot convert to native type: %v", typeDesc)
}
func (p *JSONPatchVal) ConvertToType(typeValue ref.Type) ref.Val {
if typeValue == jsonPatchType {
return p
} else if typeValue == types.TypeType {
return types.NewTypeTypeWithParam(jsonPatchType)
}
return types.NewErr("Unsupported type: %s", typeValue.TypeName())
}
func (p *JSONPatchVal) Equal(other ref.Val) ref.Val {
if o, ok := other.(*JSONPatchVal); ok && p != nil && o != nil {
if *p == *o {
return types.True
}
}
return types.False
}
func (p *JSONPatchVal) Get(index ref.Val) ref.Val {
if name, ok := index.Value().(string); ok {
switch name {
case jsonPatchOp:
return types.String(p.Op)
case jsonPatchPath:
return types.String(p.Path)
case jsonPatchFrom:
return types.String(p.From)
case jsonPatchValue:
return p.Val
default:
}
}
return types.NewErr("unsupported indexer: %s", index)
}
func (p *JSONPatchVal) IsSet(field ref.Val) ref.Val {
if name, ok := field.Value().(string); ok {
switch name {
case jsonPatchOp:
return types.Bool(len(p.Op) > 0)
case jsonPatchPath:
return types.Bool(len(p.Path) > 0)
case jsonPatchFrom:
return types.Bool(len(p.From) > 0)
case jsonPatchValue:
return types.Bool(p.Val != nil)
}
}
return types.NewErr("unsupported field: %s", field)
}
func (p *JSONPatchVal) Type() ref.Type {
return jsonPatchType
}
func (p *JSONPatchVal) Value() any {
return p
}
var _ ref.Val = &JSONPatchVal{}

View File

@ -1,110 +0,0 @@
/*
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

@ -1,147 +0,0 @@
/*
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

@ -1,100 +0,0 @@
/*
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)
}
// FindStructFieldNames returns the field names associated with the type, if the type
// is found.
func (p *TypeProvider) FindStructFieldNames(structType string) ([]string, bool) {
return nil, true
}
// 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

@ -1,66 +0,0 @@
/*
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,47 @@
/*
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"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
// ObjectTypeName is the name of Object types that are used to declare the types of
// Kubernetes objects in CEL dynamically using the naming scheme "Object.<fieldName>...<fieldName>".
// For example "Object.spec.containers" is the type of the spec.containers field of the object in scope.
const ObjectTypeName = "Object"
// JSONPatchTypeName is the name of the JSONPatch type. This type is typically used to create JSON patches
// in CEL expressions.
const JSONPatchTypeName = "JSONPatch"
// DynamicTypeResolver resolves the Object and JSONPatch types when compiling
// CEL expressions without schema information about the object.
type DynamicTypeResolver struct{}
func (r *DynamicTypeResolver) Resolve(name string) (common.ResolvedType, bool) {
if name == JSONPatchTypeName {
return &JSONPatchType{}, true
}
if name == ObjectTypeName || strings.HasPrefix(name, ObjectTypeName+".") {
return dynamic.NewObjectType(name), true
}
return nil, false
}

View File

@ -0,0 +1,280 @@
/*
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"
"strings"
"testing"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
)
func TestTypeResolver(t *testing.T) {
for _, tc := range []struct {
name string
expression string
expectedValue any
expectCompileError string
}{
{
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: "map string-to-string",
expression: `Object{
annotations: {"foo": "bar"}
}`,
expectedValue: map[string]any{
"annotations": map[string]any{
"foo": "bar",
},
},
},
{
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,
},
{
name: "invalid type",
expression: "Invalid{}",
expectCompileError: "undeclared reference to 'Invalid'",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := common.NewResolverTypeProviderAndEnvOption(&DynamicTypeResolver{})
env := mustCreateEnv(t, option)
ast, issues := env.Compile(tc.expression)
if len(tc.expectCompileError) > 0 {
if issues == nil {
t.Fatalf("expected error %v but got no error", tc.expectCompileError)
}
if !strings.Contains(issues.String(), tc.expectCompileError) {
t.Fatalf("expected error %v but got %v", tc.expectCompileError, issues.String())
}
return
}
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)
}
})
}
}
// 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
}{
{
name: "construct existing field with none, empty object",
expression: `Object{
?existing: optional.none()
}`,
expectedVal: dynamic.NewObjectVal(types.NewObjectType("Object"), 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: dynamic.NewObjectVal(types.NewObjectType("Object"), map[string]ref.Val{
// "existing" field was not set.
}),
},
{
name: "access existing field, return none",
expression: `Object{}.?existing`,
expectedVal: types.OptionalNone,
},
{
name: "map non-existing field, return none",
expression: `{"foo": 1}[?"bar"]`,
expectedVal: types.OptionalNone,
},
{
name: "map existing field, return actual value",
expression: `{"foo": 1}[?"foo"]`,
expectedVal: types.OptionalOf(types.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: types.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: types.True,
},
} {
t.Run(tc.name, func(t *testing.T) {
_, option := common.NewResolverTypeProviderAndEnvOption(&DynamicTypeResolver{})
env := mustCreateEnvWithOptional(t, option)
ast, issues := env.Compile(tc.expression)
if len(tc.expectedCompileError) > 0 {
if issues == nil {
t.Fatalf("expected error %v but got no error", tc.expectedCompileError)
}
if !strings.Contains(issues.String(), tc.expectedCompileError) {
t.Fatalf("expected error %v but got %v", tc.expectedCompileError, issues.String())
}
return
}
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 equals := tc.expectedVal.Equal(r); equals.Value() != true {
t.Errorf("expected %#+v but got %#+v", tc.expectedVal, r)
}
})
}
}
// 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(), true).
Extend(environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 0), // Always enabled. This is just for test.
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

@ -1,43 +0,0 @@
/*
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

@ -1,66 +0,0 @@
/*
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

@ -1,39 +0,0 @@
/*
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

@ -1,156 +0,0 @@
/*
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: "map string-to-string",
expression: `Object{
annotations: {"foo": "bar"}
}`,
expectedValue: map[string]any{
"annotations": map[string]any{
"foo": "bar",
},
},
},
{
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(), true).
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
}