diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go new file mode 100644 index 00000000000..685a585c70e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go new file mode 100644 index 00000000000..ae21fe07300 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/typeprovider_test.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go deleted file mode 100644 index 5eaa3a1b1b6..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/constants.go +++ /dev/null @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go deleted file mode 100644 index b2708a70719..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/interface.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go deleted file mode 100644 index aefd497159d..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go deleted file mode 100644 index b175906d008..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go new file mode 100644 index 00000000000..b00db23de4e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects.go @@ -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 or this ObjectVal +// must have type names of the form ".", children of that type must have type names of the +// form ".." 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go new file mode 100644 index 00000000000..8426986a080 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/dynamic/objects_test.go @@ -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) + } + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go deleted file mode 100644 index 911ae5db2b4..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go +++ /dev/null @@ -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())...) -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go new file mode 100644 index 00000000000..1cb018db957 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/jsonpatch.go @@ -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{} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go deleted file mode 100644 index f8d30bd299e..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go deleted file mode 100644 index 6d47f0100e5..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go deleted file mode 100644 index 694ed3c08f2..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go deleted file mode 100644 index 8e95d087372..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeprovider_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go new file mode 100644 index 00000000000..aceed5ae55e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver.go @@ -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....". +// 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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go new file mode 100644 index 00000000000..092c01770f1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/mutation/typeresolver_test.go @@ -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())...) +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go deleted file mode 100644 index f99ac516d03..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/fieldtype.go +++ /dev/null @@ -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) - }, - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go deleted file mode 100644 index 32ab8d28274..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeref.go +++ /dev/null @@ -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), - } -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go deleted file mode 100644 index 2182b931676..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver.go +++ /dev/null @@ -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 -} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go b/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go deleted file mode 100644 index 7ca743b27c3..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/cel/mutation/unstructured/typeresolver_test.go +++ /dev/null @@ -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 -}