mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-13 05:46:16 +00:00
Merge pull request #122886 from jiahuif-forks/feature/cel/mutating-library
[CEL Library] Unstructured Object Construction Support
This commit is contained in:
commit
2363cdcc39
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/cel-go/common/types/traits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootTypeReferenceName is the root reference that all type names should start with.
|
||||||
|
const RootTypeReferenceName = "Object"
|
||||||
|
|
||||||
|
// ObjectTraits is the bitmask that represents traits that an object should have.
|
||||||
|
const ObjectTraits = traits.ContainerType
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeResolver resolves a type by a given name.
|
||||||
|
type TypeResolver interface {
|
||||||
|
// Resolve resolves the type by its name, starting with "Object" as its root.
|
||||||
|
// The type that the name refers to must be an object.
|
||||||
|
// This function returns false if the name does not refer to a known object type.
|
||||||
|
Resolve(name string) (TypeRef, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeRef refers an object type that can be looked up for its fields.
|
||||||
|
type TypeRef interface {
|
||||||
|
ref.Type
|
||||||
|
|
||||||
|
// CELType wraps the TypeRef to be a type that is understood by CEL.
|
||||||
|
CELType() *types.Type
|
||||||
|
|
||||||
|
// Field finds the field by the field name, or false if the field is not known.
|
||||||
|
// This function directly return a FieldType that is known to CEL to be more customizable.
|
||||||
|
Field(name string) (*types.FieldType, bool)
|
||||||
|
|
||||||
|
// Val creates an instance for the TypeRef, given its fields and their values.
|
||||||
|
Val(fields map[string]ref.Val) ref.Val
|
||||||
|
}
|
119
staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go
Normal file
119
staging/src/k8s.io/apiserver/pkg/cel/mutation/common/val.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
"github.com/google/cel-go/common/types/traits"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectVal is the CEL Val for an object that is constructed via the object
|
||||||
|
// construction syntax.
|
||||||
|
type ObjectVal struct {
|
||||||
|
typeRef TypeRef
|
||||||
|
fields map[string]ref.Val
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewObjectVal creates an ObjectVal by its TypeRef and its fields.
|
||||||
|
func NewObjectVal(typeRef TypeRef, fields map[string]ref.Val) *ObjectVal {
|
||||||
|
return &ObjectVal{
|
||||||
|
typeRef: typeRef,
|
||||||
|
fields: fields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ref.Val = (*ObjectVal)(nil)
|
||||||
|
var _ traits.Zeroer = (*ObjectVal)(nil)
|
||||||
|
|
||||||
|
// ConvertToNative converts the object to map[string]any.
|
||||||
|
// All nested lists are converted into []any native type.
|
||||||
|
//
|
||||||
|
// It returns an error if the target type is not map[string]any,
|
||||||
|
// or any recursive conversion fails.
|
||||||
|
func (v *ObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||||
|
var result map[string]any
|
||||||
|
if typeDesc != reflect.TypeOf(result) {
|
||||||
|
return nil, fmt.Errorf("unable to convert to %v", typeDesc)
|
||||||
|
}
|
||||||
|
result = make(map[string]any, len(v.fields))
|
||||||
|
for k, v := range v.fields {
|
||||||
|
result[k] = convertField(v)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToType supports type conversions between CEL value types supported by the expression language.
|
||||||
|
func (v *ObjectVal) ConvertToType(typeValue ref.Type) ref.Val {
|
||||||
|
switch typeValue {
|
||||||
|
case v.typeRef:
|
||||||
|
return v
|
||||||
|
case types.TypeType:
|
||||||
|
return v.typeRef.CELType()
|
||||||
|
}
|
||||||
|
return types.NewErr("unsupported conversion into %v", typeValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true if the `other` value has the same type and content as the implementing struct.
|
||||||
|
func (v *ObjectVal) Equal(other ref.Val) ref.Val {
|
||||||
|
if rhs, ok := other.(*ObjectVal); ok {
|
||||||
|
return types.Bool(reflect.DeepEqual(v.fields, rhs.fields))
|
||||||
|
}
|
||||||
|
return types.Bool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the TypeValue of the value.
|
||||||
|
func (v *ObjectVal) Type() ref.Type {
|
||||||
|
return v.typeRef.CELType()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns its value as a map[string]any.
|
||||||
|
func (v *ObjectVal) Value() any {
|
||||||
|
var result any
|
||||||
|
var object map[string]any
|
||||||
|
result, err := v.ConvertToNative(reflect.TypeOf(object))
|
||||||
|
if err != nil {
|
||||||
|
return types.WrapErr(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZeroValue indicates whether the object is the zero value for the type.
|
||||||
|
// For the ObjectVal, it is zero value if and only if the fields map is empty.
|
||||||
|
func (v *ObjectVal) IsZeroValue() bool {
|
||||||
|
return len(v.fields) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertField converts a referred ref.Val to its expected type.
|
||||||
|
// For objects, the expected type is map[string]any
|
||||||
|
// For lists, the expected type is []any
|
||||||
|
// For anything else, it is converted via value.Value()
|
||||||
|
func convertField(value ref.Val) any {
|
||||||
|
// special handling for lists, where the elements are converted with Value() instead of ConvertToNative
|
||||||
|
// to allow them to become native value of any type.
|
||||||
|
if listOfVal, ok := value.Value().([]ref.Val); ok {
|
||||||
|
var result []any
|
||||||
|
for _, v := range listOfVal {
|
||||||
|
result = append(result, v.Value())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return value.Value()
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOptional(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
fields map[string]ref.Val
|
||||||
|
expected map[string]any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "present",
|
||||||
|
fields: map[string]ref.Val{
|
||||||
|
"zero": types.OptionalOf(types.IntZero),
|
||||||
|
},
|
||||||
|
expected: map[string]any{
|
||||||
|
"zero": int64(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "none",
|
||||||
|
fields: map[string]ref.Val{
|
||||||
|
"absent": types.OptionalNone,
|
||||||
|
},
|
||||||
|
expected: map[string]any{
|
||||||
|
// right now no way to differ from a plain null.
|
||||||
|
// we will need to filter out optional.none() before this conversion.
|
||||||
|
"absent": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
v := &ObjectVal{
|
||||||
|
typeRef: nil, // safe in this test, otherwise put a mock
|
||||||
|
fields: tc.fields,
|
||||||
|
}
|
||||||
|
converted := v.Value()
|
||||||
|
if !reflect.DeepEqual(tc.expected, converted) {
|
||||||
|
t.Errorf("wrong result, expected %v but got %v", tc.expected, converted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
51
staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go
Normal file
51
staging/src/k8s.io/apiserver/pkg/cel/mutation/env_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mutation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mustCreateEnv creates the default env for testing, with given option.
|
||||||
|
// it fatally fails the test if the env fails to set up.
|
||||||
|
func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
|
||||||
|
envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).
|
||||||
|
Extend(environment.VersionedOptions{
|
||||||
|
IntroducedVersion: version.MajorMinor(1, 30),
|
||||||
|
EnvOptions: envOptions,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fail to create env set: %v", err)
|
||||||
|
}
|
||||||
|
env, err := envSet.Env(environment.StoredExpressions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fail to setup env: %v", env)
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustCreateEnvWithOptional creates the default env for testing, with given option,
|
||||||
|
// and set up the optional library with default configuration.
|
||||||
|
// it fatally fails the test if the env fails to set up.
|
||||||
|
func mustCreateEnvWithOptional(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
|
||||||
|
return mustCreateEnv(t, append(envOptions, cel.OptionalTypes())...)
|
||||||
|
}
|
110
staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go
Normal file
110
staging/src/k8s.io/apiserver/pkg/cel/mutation/mock_test.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mutation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockTypeResolver is a mock implementation of TypeResolver that
|
||||||
|
// allows the object to contain any field.
|
||||||
|
type mockTypeResolver struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockTypeRef is a mock implementation of TypeRef that
|
||||||
|
// contains any field.
|
||||||
|
type mockTypeRef struct {
|
||||||
|
objectType *types.Type
|
||||||
|
resolver common.TypeResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockTypeRef(resolver common.TypeResolver, name string) *mockTypeRef {
|
||||||
|
objectType := types.NewObjectType(name, common.ObjectTraits)
|
||||||
|
return &mockTypeRef{
|
||||||
|
objectType: objectType,
|
||||||
|
resolver: resolver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeRef) HasTrait(trait int) bool {
|
||||||
|
return common.ObjectTraits|trait != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeRef) TypeName() string {
|
||||||
|
return m.objectType.TypeName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeRef) CELType() *types.Type {
|
||||||
|
return types.NewTypeTypeWithParam(m.objectType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeRef) Field(name string) (*types.FieldType, bool) {
|
||||||
|
return &types.FieldType{
|
||||||
|
Type: types.DynType,
|
||||||
|
IsSet: func(target any) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
GetFrom: func(target any) (any, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeRef) Val(fields map[string]ref.Val) ref.Val {
|
||||||
|
return common.NewObjectVal(m, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeResolver) Resolve(name string) (common.TypeRef, bool) {
|
||||||
|
if strings.HasPrefix(name, common.RootTypeReferenceName) {
|
||||||
|
return newMockTypeRef(m, name), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockTypeResolverForOptional behaves the same as mockTypeResolver
|
||||||
|
// except returning a mockTypeRefForOptional instead of mockTypeRef
|
||||||
|
type mockTypeResolverForOptional struct {
|
||||||
|
*mockTypeResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockTypeRefForOptional behaves the same as the underlying TypeRef
|
||||||
|
// except treating "nonExisting" field as non-existing.
|
||||||
|
// This is used for optional tests.
|
||||||
|
type mockTypeRefForOptional struct {
|
||||||
|
common.TypeRef
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field returns a mock FieldType, or false if the field should not exist.
|
||||||
|
func (m *mockTypeRefForOptional) Field(name string) (*types.FieldType, bool) {
|
||||||
|
if name == "nonExisting" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return m.TypeRef.Field(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTypeResolverForOptional) Resolve(name string) (common.TypeRef, bool) {
|
||||||
|
r, ok := m.mockTypeResolver.Resolve(name)
|
||||||
|
if ok {
|
||||||
|
return &mockTypeRefForOptional{TypeRef: r}, ok
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
147
staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go
Normal file
147
staging/src/k8s.io/apiserver/pkg/cel/mutation/optional_test.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mutation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCELOptional is an exploration test to demonstrate how CEL optional library
|
||||||
|
// behave for the use cases that the mutation library requires.
|
||||||
|
func TestCELOptional(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
expectedVal ref.Val
|
||||||
|
expectedCompileError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// question mark syntax still requires the field to exist in object construction
|
||||||
|
name: "construct non-existing field, compile error",
|
||||||
|
expression: `Object{
|
||||||
|
?nonExisting: optional.none()
|
||||||
|
}`,
|
||||||
|
expectedCompileError: `undefined field 'nonExisting'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The root cause of the behavior above is that, has on an object (or Message in the Language Def),
|
||||||
|
// still require the field to be declared in the schema.
|
||||||
|
//
|
||||||
|
// Quoting from
|
||||||
|
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
|
||||||
|
//
|
||||||
|
// To test for the presence of a field, the boolean-valued macro has(e.f) can be used.
|
||||||
|
//
|
||||||
|
// 2. If e evaluates to a message and f is not a declared field for the message,
|
||||||
|
// has(e.f) raises a no_such_field error.
|
||||||
|
name: "has(Object{}), de-sugared, compile error",
|
||||||
|
expression: "has(Object{}.nonExisting)",
|
||||||
|
expectedCompileError: `undefined field 'nonExisting'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "construct existing field with none, empty object",
|
||||||
|
expression: `Object{
|
||||||
|
?existing: optional.none()
|
||||||
|
}`,
|
||||||
|
expectedVal: common.NewObjectVal(nil, map[string]ref.Val{
|
||||||
|
// "existing" field was not set.
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object of zero value, ofNonZeroValue",
|
||||||
|
expression: `Object{?spec: optional.ofNonZeroValue(Object.spec{?replicas: Object{}.?replicas})}`,
|
||||||
|
expectedVal: common.NewObjectVal(nil, map[string]ref.Val{
|
||||||
|
// "existing" field was not set.
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "access non-existing field, return none",
|
||||||
|
expression: `Object{}.?nonExisting`,
|
||||||
|
expectedCompileError: `undefined field 'nonExisting'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "access existing field, return none",
|
||||||
|
expression: `Object{}.?existing`,
|
||||||
|
expectedVal: celtypes.OptionalNone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map non-existing field, return none",
|
||||||
|
expression: `{"foo": 1}[?"bar"]`,
|
||||||
|
expectedVal: celtypes.OptionalNone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "map existing field, return actual value",
|
||||||
|
expression: `{"foo": 1}[?"foo"]`,
|
||||||
|
expectedVal: celtypes.OptionalOf(celtypes.Int(1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Map has a different behavior than Object
|
||||||
|
//
|
||||||
|
// Quoting from
|
||||||
|
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
|
||||||
|
//
|
||||||
|
// To test for the presence of a field, the boolean-valued macro has(e.f) can be used.
|
||||||
|
//
|
||||||
|
// 1. If e evaluates to a map, then has(e.f) indicates whether the string f is
|
||||||
|
// a key in the map (note that f must syntactically be an identifier).
|
||||||
|
//
|
||||||
|
name: "has on a map, de-sugared, non-existing field, returns false",
|
||||||
|
// has marco supports only the dot access syntax.
|
||||||
|
expression: `has({"foo": 1}.bar)`,
|
||||||
|
expectedVal: celtypes.False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has on a map, de-sugared, existing field, returns true",
|
||||||
|
// has marco supports only the dot access syntax.
|
||||||
|
expression: `has({"foo": 1}.foo)`,
|
||||||
|
expectedVal: celtypes.True,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, option := NewTypeProviderAndEnvOption(&mockTypeResolverForOptional{
|
||||||
|
mockTypeResolver: &mockTypeResolver{},
|
||||||
|
})
|
||||||
|
env := mustCreateEnvWithOptional(t, option)
|
||||||
|
ast, issues := env.Compile(tc.expression)
|
||||||
|
if issues != nil {
|
||||||
|
if tc.expectedCompileError == "" {
|
||||||
|
t.Fatalf("unexpected issues during compilation: %v", issues)
|
||||||
|
} else if !strings.Contains(issues.String(), tc.expectedCompileError) {
|
||||||
|
t.Fatalf("unexpected compile error, want to contain %q but got %v", tc.expectedCompileError, issues)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
program, err := env.Program(ast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error while creating program: %v", err)
|
||||||
|
}
|
||||||
|
r, _, err := program.Eval(map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error during evaluation: %v", err)
|
||||||
|
}
|
||||||
|
if equals := tc.expectedVal.Equal(r); equals.Value() != true {
|
||||||
|
t.Errorf("expected %v but got %v", tc.expectedVal, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mutation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation/common"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeProvider is a specialized CEL type provider that understands
|
||||||
|
// the Object type alias that is used to construct an Apply configuration for
|
||||||
|
// a mutation operation.
|
||||||
|
type TypeProvider struct {
|
||||||
|
typeResolver common.TypeResolver
|
||||||
|
underlyingTypeProvider types.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.Provider = (*TypeProvider)(nil)
|
||||||
|
|
||||||
|
// EnumValue returns the numeric value of the given enum value name.
|
||||||
|
// This TypeProvider does not have special handling for EnumValue and thus directly delegate
|
||||||
|
// to its underlying type provider.
|
||||||
|
func (p *TypeProvider) EnumValue(enumName string) ref.Val {
|
||||||
|
return p.underlyingTypeProvider.EnumValue(enumName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindIdent takes a qualified identifier name and returns a ref.ObjectVal if one exists.
|
||||||
|
// This TypeProvider does not have special handling for FindIdent and thus directly delegate
|
||||||
|
// to its underlying type provider.
|
||||||
|
func (p *TypeProvider) FindIdent(identName string) (ref.Val, bool) {
|
||||||
|
return p.underlyingTypeProvider.FindIdent(identName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindStructType returns the Type give a qualified type name, by looking it up with
|
||||||
|
// the TypeResolver and translating it to CEL Type.
|
||||||
|
// If the type is not known to the TypeResolver, the lookup falls back to the underlying
|
||||||
|
// TypeProvider instead.
|
||||||
|
func (p *TypeProvider) FindStructType(structType string) (*types.Type, bool) {
|
||||||
|
t, ok := p.typeResolver.Resolve(structType)
|
||||||
|
if ok {
|
||||||
|
return t.CELType(), true
|
||||||
|
}
|
||||||
|
return p.underlyingTypeProvider.FindStructType(structType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindStructFieldType returns the field type for a checked type value.
|
||||||
|
// Returns false if the field could not be found.
|
||||||
|
func (p *TypeProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) {
|
||||||
|
t, ok := p.typeResolver.Resolve(structType)
|
||||||
|
if ok {
|
||||||
|
return t.Field(fieldName)
|
||||||
|
}
|
||||||
|
return p.underlyingTypeProvider.FindStructFieldType(structType, fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValue creates a new type value from a qualified name and map of fields.
|
||||||
|
func (p *TypeProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val {
|
||||||
|
t, ok := p.typeResolver.Resolve(structType)
|
||||||
|
if ok {
|
||||||
|
return t.Val(fields)
|
||||||
|
}
|
||||||
|
return p.underlyingTypeProvider.NewValue(structType, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTypeProviderAndEnvOption creates the TypeProvider with a given TypeResolver,
|
||||||
|
// and also returns the CEL EnvOption to apply it to the env.
|
||||||
|
func NewTypeProviderAndEnvOption(resolver common.TypeResolver) (*TypeProvider, cel.EnvOption) {
|
||||||
|
tp := &TypeProvider{typeResolver: resolver}
|
||||||
|
var envOption cel.EnvOption = func(e *cel.Env) (*cel.Env, error) {
|
||||||
|
// wrap the existing type provider (acquired from the env)
|
||||||
|
// and set new type provider for the env.
|
||||||
|
tp.underlyingTypeProvider = e.CELTypeProvider()
|
||||||
|
typeProviderOption := cel.CustomTypeProvider(tp)
|
||||||
|
return typeProviderOption(e)
|
||||||
|
}
|
||||||
|
return tp, envOption
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package mutation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTypeProvider(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
expectedValue any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not an object",
|
||||||
|
expression: `2 * 31 * 1847`,
|
||||||
|
expectedValue: int64(114514), // type resolver should not interfere.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
expression: "Object{}",
|
||||||
|
expectedValue: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Object.spec",
|
||||||
|
expression: "Object{spec: Object.spec{replicas: 3}}",
|
||||||
|
expectedValue: map[string]any{"spec": map[string]any{"replicas": int64(3)}},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, option := NewTypeProviderAndEnvOption(&mockTypeResolver{})
|
||||||
|
env := mustCreateEnv(t, option)
|
||||||
|
ast, issues := env.Compile(tc.expression)
|
||||||
|
if issues != nil {
|
||||||
|
t.Fatalf("unexpected issues during compilation: %v", issues)
|
||||||
|
}
|
||||||
|
program, err := env.Program(ast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error while creating program: %v", err)
|
||||||
|
}
|
||||||
|
r, _, err := program.Eval(map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error during evaluation: %v", err)
|
||||||
|
}
|
||||||
|
if v := r.Value(); !reflect.DeepEqual(tc.expectedValue, v) {
|
||||||
|
t.Errorf("expected %v but got %v", tc.expectedValue, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package unstructured
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFieldType creates a field by its field name.
|
||||||
|
// This version of FieldType is unstructured and has DynType as its type.
|
||||||
|
func NewFieldType(name string) *types.FieldType {
|
||||||
|
return &types.FieldType{
|
||||||
|
// for unstructured, we do not check for its type,
|
||||||
|
// use DynType for all fields.
|
||||||
|
Type: types.DynType,
|
||||||
|
IsSet: func(target any) bool {
|
||||||
|
// for an unstructured object, we allow any field to be considered set.
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
GetFrom: func(target any) (any, error) {
|
||||||
|
if m, ok := target.(map[string]any); ok {
|
||||||
|
return m[name], nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("cannot get field %q", name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package unstructured
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeRef is the implementation of TypeRef for an unstructured object.
|
||||||
|
// This is especially usefully when the schema is not known or available.
|
||||||
|
type TypeRef struct {
|
||||||
|
celObjectType *types.Type
|
||||||
|
celTypeType *types.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TypeRef) HasTrait(trait int) bool {
|
||||||
|
return common.ObjectTraits|trait != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeName returns the name of this TypeRef.
|
||||||
|
func (r *TypeRef) TypeName() string {
|
||||||
|
return r.celObjectType.TypeName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Val returns an instance given the fields.
|
||||||
|
func (r *TypeRef) Val(fields map[string]ref.Val) ref.Val {
|
||||||
|
return common.NewObjectVal(r, fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CELType returns the type. The returned type is of TypeType type.
|
||||||
|
func (r *TypeRef) CELType() *types.Type {
|
||||||
|
return r.celTypeType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field looks up the field by name.
|
||||||
|
// This is the unstructured version that allows any name as the field name.
|
||||||
|
// The returned field is of DynType type.
|
||||||
|
func (r *TypeRef) Field(name string) (*types.FieldType, bool) {
|
||||||
|
return NewFieldType(name), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTypeRef creates a TypeRef by the given field name.
|
||||||
|
func NewTypeRef(name string) *TypeRef {
|
||||||
|
objectType := types.NewObjectType(name, common.ObjectTraits)
|
||||||
|
return &TypeRef{
|
||||||
|
celObjectType: objectType,
|
||||||
|
celTypeType: types.NewTypeTypeWithParam(objectType),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package unstructured
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const object = common.RootTypeReferenceName
|
||||||
|
|
||||||
|
type TypeResolver struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve resolves the TypeRef for the given type name
|
||||||
|
// that starts with "Object".
|
||||||
|
// This is the unstructured version, which means the
|
||||||
|
// returned TypeRef does not refer to the schema.
|
||||||
|
func (r *TypeResolver) Resolve(name string) (common.TypeRef, bool) {
|
||||||
|
if !strings.HasPrefix(name, object) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return NewTypeRef(name), true
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package unstructured
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
|
"k8s.io/apiserver/pkg/cel/mutation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTypeProvider(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
expectedValue any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not an object",
|
||||||
|
expression: `string(114514)`,
|
||||||
|
expectedValue: "114514",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
expression: "Object{}",
|
||||||
|
expectedValue: map[string]any{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Object.spec",
|
||||||
|
expression: "Object{spec: Object.spec{replicas: 3}}",
|
||||||
|
expectedValue: map[string]any{
|
||||||
|
"spec": map[string]any{
|
||||||
|
// an integer maps to int64
|
||||||
|
"replicas": int64(3),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// list literal does not require new path code of the type provider
|
||||||
|
// comparing to the object literal.
|
||||||
|
// This test case serves as a note of "supported syntax"
|
||||||
|
name: "Object.spec.template.containers",
|
||||||
|
expression: `Object{
|
||||||
|
spec: Object.spec{
|
||||||
|
template: Object.spec.template{
|
||||||
|
containers: [
|
||||||
|
Object.spec.template.containers.item{
|
||||||
|
name: "nginx",
|
||||||
|
image: "nginx",
|
||||||
|
args: ["-g"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expectedValue: map[string]any{
|
||||||
|
"spec": map[string]any{
|
||||||
|
"template": map[string]any{
|
||||||
|
"containers": []any{
|
||||||
|
map[string]any{
|
||||||
|
"name": "nginx",
|
||||||
|
"image": "nginx",
|
||||||
|
"args": []any{"-g"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list of ints",
|
||||||
|
expression: `Object{
|
||||||
|
intList: [1, 2, 3]
|
||||||
|
}`,
|
||||||
|
expectedValue: map[string]any{
|
||||||
|
"intList": []any{int64(1), int64(2), int64(3)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "field access",
|
||||||
|
expression: `Object{
|
||||||
|
intList: [1, 2, 3]
|
||||||
|
}.intList.sum()`,
|
||||||
|
expectedValue: int64(6),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equality check",
|
||||||
|
expression: "Object{spec: Object.spec{replicas: 3}} == Object{spec: Object.spec{replicas: 1 + 2}}",
|
||||||
|
expectedValue: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
_, option := mutation.NewTypeProviderAndEnvOption(&TypeResolver{})
|
||||||
|
env := mustCreateEnv(t, option)
|
||||||
|
ast, issues := env.Compile(tc.expression)
|
||||||
|
if issues != nil {
|
||||||
|
t.Fatalf("unexpected issues during compilation: %v", issues)
|
||||||
|
}
|
||||||
|
program, err := env.Program(ast)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error while creating program: %v", err)
|
||||||
|
}
|
||||||
|
r, _, err := program.Eval(map[string]any{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error during evaluation: %v", err)
|
||||||
|
}
|
||||||
|
if v := r.Value(); !reflect.DeepEqual(v, tc.expectedValue) {
|
||||||
|
t.Errorf("expected %v but got %v", tc.expectedValue, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func mustCreateEnv(t testing.TB, envOptions ...cel.EnvOption) *cel.Env {
|
||||||
|
envSet, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).
|
||||||
|
Extend(environment.VersionedOptions{
|
||||||
|
IntroducedVersion: version.MajorMinor(1, 30),
|
||||||
|
EnvOptions: envOptions,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fail to create env set: %v", err)
|
||||||
|
}
|
||||||
|
env, err := envSet.Env(environment.StoredExpressions)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fail to setup env: %v", env)
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user