Merge pull request #122886 from jiahuif-forks/feature/cel/mutating-library
[CEL Library] Unstructured Object Construction Support Kubernetes-commit: 2363cdcc399cbf428210efb2c51575ddcad2b84a
This commit is contained in:
commit
0dd0e74922
8
go.mod
8
go.mod
|
@ -42,10 +42,10 @@ require (
|
|||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70
|
||||
k8s.io/api v0.0.0-20240124211858-f3648a53522e
|
||||
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523
|
||||
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605
|
||||
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48
|
||||
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114
|
||||
k8s.io/klog/v2 v2.120.1
|
||||
k8s.io/kms v0.0.0-20231220174908-0e979309a09f
|
||||
k8s.io/kube-openapi v0.0.0-20231113174909-778a5567bc1e
|
||||
|
@ -125,9 +125,9 @@ require (
|
|||
)
|
||||
|
||||
replace (
|
||||
k8s.io/api => k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70
|
||||
k8s.io/api => k8s.io/api v0.0.0-20240124211858-f3648a53522e
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523
|
||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20240124011219-8092c71d3605
|
||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48
|
||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114
|
||||
k8s.io/kms => k8s.io/kms v0.0.0-20231220174908-0e979309a09f
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -385,14 +385,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70 h1:RC1/ThLmvqAGg+Rf6TdfZ14G0AwXXxRxgQVXO1HtJYk=
|
||||
k8s.io/api v0.0.0-20240124211857-8b0ccc2e7c70/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA=
|
||||
k8s.io/api v0.0.0-20240124211858-f3648a53522e h1:Lv52wennNKzlcDrBtANztawHC8xaTllHm51WUKIP0Ew=
|
||||
k8s.io/api v0.0.0-20240124211858-f3648a53522e/go.mod h1:BZUGwl6J5EvsODp+6ZUA+9p7V4iWxVLcr70rnzIshpA=
|
||||
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523 h1:1iJCbQAZv58v4zxd0ECIIMnyYlFsPWa2hmjqGEsv/5g=
|
||||
k8s.io/apimachinery v0.0.0-20240118211638-f14778da5523/go.mod h1:Oh3ZrffM1/I8O/43oAA+aoOYgSregIXHxcWJB9ZRfQ8=
|
||||
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605 h1:Dw3Ctw+SS3YmJTpaYP2nhIs4XagL4ctjKY0pHxN4RT8=
|
||||
k8s.io/client-go v0.0.0-20240124011219-8092c71d3605/go.mod h1:WuuT9L6+pj4rHmL2pb22xnOdtSvjiEcpB18g9Fuk0js=
|
||||
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48 h1:3HvTUZ0ry5c0P15P+glBxBj+eh8Uv2ijNvjEORH+oOQ=
|
||||
k8s.io/component-base v0.0.0-20240123212339-5f9f8131aa48/go.mod h1:ANnr9YwsqK1XgjzXj9fGHEMDOp0QddDkKgQLLBPZ7Kg=
|
||||
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114 h1:7t7EBTKztnJ9XSMAc6ogXdhhv/EgWCz0tIn3uiNfmjY=
|
||||
k8s.io/component-base v0.0.0-20240125212330-b7222f6d9114/go.mod h1:fqdOK4GDgnyAlr6wC5fhFijh1aJ4WNcEQOcn7VaylqA=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.0.0-20231220174908-0e979309a09f h1:rfX7Jxf6fLy/4qh6tWv+hwn7z25oYr6yZkcVVSSZqnE=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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())...)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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