apiserver/pkg/cel/common/values_test.go

1371 lines
42 KiB
Go

/*
Copyright 2025 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_test
import (
"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/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/kube-openapi/pkg/validation/spec"
"testing"
"time"
)
// TestToValue tests that both UnstructuredToValue and TypedToValue correctly
// convert Kubernetes typed objects and unstructured objects to CEL values.
//
// TestToValue also tests that UnstructuredToValue and TypedToValue behave identically
// for a Kubernetes object.
func TestToValue(t *testing.T) {
struct1value := Struct{S: "hello", I: 10, B: true, F: 1.5}
struct1 := typedValue{value: struct1value, schema: structSchema}
structOmitEmpty1value := StructOmitEmpty{}
structOmitEmpty1 := typedValue{value: structOmitEmpty1value, schema: structSchema}
structOmitZero1value := StructOmitZero{}
structOmitZero1 := typedValue{value: structOmitZero1value, schema: structSchema}
struct1Ptr := typedValue{value: &struct1value, schema: structSchema}
struct2value := Struct{S: "world", I: 20, B: false, F: 2.5}
struct2 := typedValue{value: struct2value, schema: structSchema}
struct1Again := typedValue{value: struct1value, schema: structSchema}
zeroStruct := typedValue{value: Struct{}, schema: structSchema}
zeroStructPtr := typedValue{value: Struct{}, schema: structSchema}
now := metav1.NewTime(time.Now().Truncate(0))
duration1 := metav1.Duration{Duration: 5 * time.Second}
time1Parsed, err := time.Parse(RFC3339, "2000-01-01T12:00:00Z")
if err != nil {
t.Fatal(err)
}
time1 := metav1.Time{Time: time1Parsed}
microTime1Parsed, err := time.Parse(RFC3339Micro, "2000-01-01T12:00:00.000001Z")
if err != nil {
t.Fatal(err)
}
microTime1 := metav1.MicroTime{Time: microTime1Parsed}
nested1value := Nested{Name: "nested1", Info: Struct{S: "hello", I: 10, B: true, F: 1.5}}
complex1value := Complex{
TypeMeta: metav1.TypeMeta{Kind: "Complex", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "complex1"},
ID: "c1",
Tags: []string{"a", "b", "c"},
Labels: map[string]string{"key1": "val1", "key2": "val2"},
NestedObj: nested1value,
NestedEmpty: Nested{},
NestedZero: Nested{},
Timeout: duration1,
Time: time1,
MicroTime: microTime1,
RawBytes: []byte("bytes1"),
NilBytes: nil,
ChildPtr: &struct2value,
NilPtr: nil,
EmptySlice: []int{},
NilSlice: nil,
EmptyMap: map[string]int{},
NilMap: nil,
IntOrString: intstr.FromInt32(5),
Quantity: resource.MustParse("100m"),
I32: int32(32),
I64: int64(64),
F32: float32(32.5),
Enum: EnumTypeA,
MapList: []MapListEntry{
{
Key1: "k1v1",
Key2: "k2v1",
Value: 1,
},
{
Key1: "k1v2",
Key2: "k2v2",
Value: 2,
},
},
SetList: []SetEntry{1, 2, 3},
}
complex1 := typedValue{value: complex1value, schema: complexSchema}
complex2value := Complex{
TypeMeta: metav1.TypeMeta{Kind: "Complex2", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "complex2"},
ID: "c2",
Tags: []string{"x", "y"},
Labels: map[string]string{"key3": "val3"},
NestedObj: Nested{Name: "nested2", Info: struct2value},
NestedEmpty: Nested{Name: "nested3"},
NestedZero: Nested{Name: "nested4"},
Timeout: metav1.Duration{Duration: 10 * time.Second},
RawBytes: []byte("bytes2"),
NilBytes: []byte{}, // Non-nil but empty
ChildPtr: &struct1value,
NilPtr: nil,
EmptySlice: []int{1}, // Non-empty
NilSlice: []int{1}, // Non-nil
EmptyMap: map[string]int{"a": 1}, // Non-empty
NilMap: map[string]int{"a": 1}, // Non-nil
IntOrString: intstr.FromString("port"),
Quantity: resource.MustParse("200m"),
I32: int32(42),
I64: int64(200),
F32: float32(42.5),
Enum: EnumTypeB,
MapList: []MapListEntry{
{
Key1: "k1v2",
Key2: "k2v2",
Value: 2,
},
{
Key1: "k1v1",
Key2: "k2v1",
Value: 1,
},
},
SetList: []SetEntry{3, 2, 1},
}
complex2 := typedValue{value: complex2value, schema: complexSchema}
complex3value := Complex{
MapList: []MapListEntry{
{
Key1: "k1v3",
Key2: "k2v3",
Value: 3,
},
{
Key1: "k1v1",
Key2: "k2v1",
Value: 1,
},
},
SetList: []SetEntry{4, 1},
}
complex3 := typedValue{value: complex3value, schema: complexSchema}
complex4value := Complex{
MapList: []MapListEntry{
{
Key1: "k1v3",
Key2: "k2v3",
Value: 3,
},
{
Key1: "k1v2",
Key2: "k2v2",
Value: 2,
},
{
Key1: "k1v1",
Key2: "k2v1",
Value: 1,
},
},
SetList: []SetEntry{4, 3, 2, 1},
}
complex4 := typedValue{value: complex4value, schema: complexSchema}
complex1Again := complex1 // Create a copy for equality checks
slice1 := []int{1, 2, 3}
slice1Again := []int{1, 2, 3}
slice2 := []int{1, 2, 4}
slice3 := []string{"a", "b"}
map1 := map[string]int{"a": 1, "b": 2}
map1Again := map[string]int{"b": 2, "a": 1}
map2 := map[string]int{"a": 1, "b": 3} // Different value
map3 := map[string]int{"a": 1, "c": 2} // Different key
map4 := map[string]string{"a": "1", "b": "2"} // Different value type
tests := []testCase{
// Basic Type Conversions
{
name: "basic: int32",
expression: "c.i32 == 32",
activation: map[string]typedValue{"c": complex1},
},
{
name: "basic: int64",
expression: "c.i64 == 64",
activation: map[string]typedValue{"c": complex1},
},
{
name: "basic: float32",
expression: "c.f32 == 32.5",
activation: map[string]typedValue{"c": complex1},
},
{
name: "basic: enum",
expression: "c.enum == 'a'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "basic: nil bytes",
expression: "!has(c.nilBytes)",
activation: map[string]typedValue{"c": complex1},
},
// Struct Tests
{
name: "struct: zero value struct",
expression: "obj.s == '' && obj.i == 0 && obj.b == false && obj.f == 0.0",
activation: map[string]typedValue{"obj": zeroStruct},
},
{
name: "struct: zero value struct pointer",
expression: "obj.s == '' && obj.i == 0 && obj.b == false && obj.f == 0.0",
activation: map[string]typedValue{"obj": zeroStructPtr},
},
{
name: "struct: populated struct field access",
expression: "obj.s == 'hello' && obj.i == 10 && obj.b == true && obj.f == 1.5",
activation: map[string]typedValue{"obj": struct1},
},
{
name: "struct: populated struct pointer field access",
expression: "obj.s == 'hello' && obj.i == 10 && obj.b == true && obj.f == 1.5",
activation: map[string]typedValue{"obj": struct1Ptr},
},
{
name: "struct: access omitempty field (has)",
expression: "!has(obj.so)",
activation: map[string]typedValue{"obj": struct1},
},
{
name: "struct: access non-existent field (has)",
expression: "!has(obj.nonExistent)",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "struct: access non-existent field direct (error)",
expression: "obj.nonExistent",
activation: map[string]typedValue{"obj": struct2},
wantErr: "no such key: nonExistent",
},
{
name: "struct: access with non-string key (get) (error)",
expression: "obj[1]",
activation: map[string]typedValue{"obj": struct2},
wantErr: "no such overload",
},
{
name: "struct: check contains non-string key (error)",
expression: "1 in obj",
activation: map[string]typedValue{"obj": struct2},
wantErr: "no such overload",
},
{
name: "struct: convert to its own type",
expression: "type(obj) == type(obj)",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "struct: embedded inline",
expression: "c.apiVersion == 'v1' && c.kind == 'Complex'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "struct: embedded inline: omitempty",
expression: "!has(c.apiVersion)",
activation: map[string]typedValue{"c": struct2},
},
{
name: "struct: embedded struct",
expression: "c.metadata.name == 'complex1'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "struct: omitempty: zero valued: has struct field",
expression: "has(c.nestedEmpty)",
activation: map[string]typedValue{"c": complex2},
},
{
name: "struct: omitempty: zero valued: does not have scalar fields",
expression: "!has(c.s) && !has(c.i) && !has(c.b) && !has(c.f)",
activation: map[string]typedValue{"c": structOmitEmpty1},
},
{
name: "struct: omitempty: zero valued: does not have pointer to scalar fields",
expression: "!has(c.sp) && !has(c.ip) && !has(c.bp) && !has(c.fp)",
activation: map[string]typedValue{"c": structOmitEmpty1},
},
{
name: "struct: omitzero: zero valued: does not have struct field",
expression: "!has(c.nestedZero)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "struct: omitzero: zero valued: does not have scalar fields",
expression: "!has(c.s) && !has(c.i) && !has(c.b) && !has(c.f)",
activation: map[string]typedValue{"c": structOmitZero1},
},
{
name: "struct: omitzero: zero valued: does not have pointer to scalar fields",
expression: "!has(c.sp) && !has(c.ip) && !has(c.bp) && !has(c.fp)",
activation: map[string]typedValue{"c": structOmitZero1},
},
{
name: "struct: omitzero: non-zero valued: has struct field",
expression: "has(c.nestedZero)",
activation: map[string]typedValue{"c": complex2},
},
{
name: "struct: omitempty: zero valued: has embedded struct field",
expression: "has(c.metadata)",
activation: map[string]typedValue{"c": structOmitEmpty1},
},
{
name: "struct: omitzero: zero valued: does not have embedded struct field",
expression: "!has(c.metadata)",
activation: map[string]typedValue{"c": structOmitZero1},
},
// Comparison Tests
{
name: "compare: identity (struct)",
expression: "s1 == s1",
activation: map[string]typedValue{"s1": struct1},
},
{
name: "compare: identical structs",
expression: "s1 == s1_again",
activation: map[string]typedValue{
"s1": struct1,
"s1_again": struct1Again,
},
},
{
name: "compare: different structs",
expression: "s1 != s2",
activation: map[string]typedValue{
"s1": struct1,
"s2": struct2,
},
},
{
name: "compare: identical complex structs",
expression: "c1 == c2",
activation: map[string]typedValue{"c1": complex1, "c2": complex1Again},
},
{
name: "compare: different complex structs",
expression: "c1 != c2",
activation: map[string]typedValue{"c1": complex1, "c2": complex2},
},
{
name: "compare: struct and pointer to identical struct",
expression: "s1 == s1_ptr",
activation: map[string]typedValue{
"s1": struct1,
"s1_ptr": struct1Ptr,
},
},
{
name: "compare: struct and nil",
expression: "s1 != null",
activation: map[string]typedValue{"s1": struct1},
},
{
name: "compare: struct and different type",
expression: "s1 != 10",
activation: map[string]typedValue{"s1": struct1},
},
{
name: "compare: nil struct pointer and null",
expression: "nil_obj == null",
activation: map[string]typedValue{"nil_obj": {value: nil, schema: structSchema}},
},
{
name: "compare: identical slices (activation)",
expression: "sl1 == sl1a",
activation: map[string]typedValue{
"sl1": {value: slice1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
"sl1a": {value: slice1Again, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
},
},
{
name: "compare: different slices (activation)",
expression: "sl1 != sl2",
activation: map[string]typedValue{
"sl1": {value: slice1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
"sl2": {value: slice2, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
},
},
{
name: "compare: slices of different types",
expression: "sl1 != sl3",
activation: map[string]typedValue{
"sl1": {value: slice1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
"sl3": {value: slice3, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: stringSchema}}}},
},
},
{
name: "compare: slice and non-list",
expression: "sl1 != 1",
activation: map[string]typedValue{
"sl1": {value: slice1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
},
},
{
name: "compare: identical maps (activation)",
expression: "m1 == m1a",
activation: map[string]typedValue{
"m1": {value: map1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
"m1a": {value: map1Again, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
},
},
{
name: "compare: different maps (value) (activation)",
expression: "m1 != m2",
activation: map[string]typedValue{
"m1": {value: map1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
"m2": {value: map2, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
},
},
{
name: "compare: different maps (key) (activation)",
expression: "m1 != m3",
activation: map[string]typedValue{
"m1": {value: map1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
"m3": {value: map3, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
},
},
{
name: "compare: different maps (value type)",
expression: "m1 != m4",
activation: map[string]typedValue{
"m1": {value: map1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
"m4": {value: map4, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: stringSchema}}}},
},
},
{
name: "compare: map and non-map",
expression: "m1 != 1",
activation: map[string]typedValue{
"m1": {value: map1, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
},
},
{
name: "compare: time instances (equal)",
expression: "t1 == t2",
activation: map[string]typedValue{
"t1": {value: now, schema: &timeFormat},
"t2": {value: now, schema: &timeFormat},
},
},
{
name: "compare: time instances (different)",
expression: "t1 != t2",
activation: map[string]typedValue{
"t1": {value: now, schema: &timeFormat},
"t2": {value: metav1.MicroTime{Time: now.Add(time.Nanosecond)}, schema: stringSchema},
},
},
{
name: "compare: microTime instances (equal)",
expression: "t1 == t2",
activation: map[string]typedValue{
"t1": {value: now, schema: stringSchema},
"t2": {value: now, schema: stringSchema},
},
},
{
name: "compare: microTime instances (different)",
expression: "t1 != t2",
activation: map[string]typedValue{
"t1": {value: now, schema: stringSchema},
"t2": {value: metav1.MicroTime{Time: now.Add(time.Nanosecond)}, schema: stringSchema},
},
},
{
name: "compare: duration instances (equal)",
expression: "d1 == d2",
activation: map[string]typedValue{
"d1": {value: duration1, schema: stringSchema},
"d2": {value: metav1.Duration{Duration: 5 * time.Second}, schema: stringSchema},
},
},
{
name: "compare: duration instances (different)",
expression: "d1 != d2",
activation: map[string]typedValue{
"d1": {value: duration1, schema: stringSchema},
"d2": {value: metav1.Duration{Duration: 6 * time.Second}, schema: stringSchema},
},
},
{
name: "compare: bytes instances (equal)",
expression: "b1 == b2",
activation: map[string]typedValue{
"b1": {value: []byte("abc"), schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "byte"}}},
"b2": {value: []byte("abc"), schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "byte"}}},
},
},
{
name: "compare: bytes instances (different)",
expression: "b1 != b2",
activation: map[string]typedValue{
"b1": {value: []byte("abc"), schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "byte"}}},
"b2": {value: []byte("abd"), schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "byte"}}},
},
},
{
name: "compare: empty slices (different underlying types)",
expression: "e1 == e2",
activation: map[string]typedValue{
"e1": {value: []int{}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: int64Schema}}}},
"e2": {value: []string{}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: stringSchema}}}},
},
},
{
name: "compare: empty maps (different underlying types)",
expression: "m1 == m2",
activation: map[string]typedValue{
"m1": {value: map[string]int{}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: int64Schema}}}},
"m2": {value: map[string]bool{}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"object"}, AdditionalProperties: &spec.SchemaOrBool{Schema: boolSchema}}}},
},
},
// Nested Struct Tests
{
name: "nested: access field",
expression: "c.nestedObj.info.s == 'hello'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "nested: compare nested struct",
expression: "c1.nestedObj != c2.nestedObj",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "nested: compare identical nested struct",
expression: "c1.nestedObj == c1_again.nestedObj",
activation: map[string]typedValue{
"c1": complex1,
"c1_again": complex1Again,
},
},
// Slice Tests
{
name: "slice: access element",
expression: "c.tags[1] == 'b'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: size",
expression: "size(c.tags) == 3",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: contains ('in')",
expression: "'b' in c.tags",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: not contains ('in')",
expression: "!('d' in c.tags)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: contains with non-primitive (struct)",
expression: "s1 in structs",
activation: map[string]typedValue{
"structs": {value: []Struct{struct2value, struct1value}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: structSchema}}}},
"s1": struct1,
},
},
{
name: "slice: contains with non-primitive (struct ptr)",
expression: "s1 in structs",
activation: map[string]typedValue{
"structs": {value: []*Struct{&struct2value, &struct1value}, schema: &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{Schema: structSchema}}}},
"s1": struct1,
},
},
{
name: "slice: add",
expression: "size(c1.tags + c2.tags) == 5 && (c1.tags + c2.tags)[3] == 'x'",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "slice: add non-list (error)",
expression: "c.tags + 1",
activation: map[string]typedValue{
"c": complex1,
},
wantErr: "no such overload",
},
{
name: "slice: get with non-int index (error)",
expression: `c.tags['a']`,
activation: map[string]typedValue{
"c": complex1,
},
wantErr: `unsupported index type 'string' in list`,
},
{
name: "slice: all() true",
expression: "c.tags.all(t, t.startsWith(''))",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: all() false",
expression: "!c.tags.all(t, t == 'a')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: exists() true",
expression: "c.tags.exists(t, t == 'c')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: exists() false",
expression: "!c.tags.exists(t, t == 'z')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: out of bounds access",
expression: "c.tags[5]",
activation: map[string]typedValue{"c": complex1},
wantErr: "index out of bounds: 5",
},
{
name: "slice: empty slice size",
expression: "size(c.emptySlice) == 0",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: exists() on empty",
expression: "!c.emptySlice.exists(x, true)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: all() on empty",
expression: "c.emptySlice.all(x, false)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: convert to list type",
expression: "type(c.tags) == list",
activation: map[string]typedValue{"c": complex1},
},
{
name: "slice: convert list to type type",
expression: "type(c.tags) == list",
activation: map[string]typedValue{"c": complex1},
},
// Map Tests
{
name: "map: access element",
expression: "c.labels['key1'] == 'val1'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: size",
expression: "size(c.labels) == 2",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: contains key ('in')",
expression: "'key1' in c.labels",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: not contains key ('in')",
expression: "!('key3' in c.labels)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: has() key",
expression: "has(c.labels.key1)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: has() non-existent key",
expression: "!has(c.labels.key3)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: access non-existent key (error)",
expression: "c.labels['key3']",
activation: map[string]typedValue{"c": complex1},
wantErr: "no such key: key3",
},
{
name: "map: all() on keys true",
expression: "c.labels.all(name, name.startsWith('key'))",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: all() on keys false",
expression: "!c.labels.all(name, name == 'key1')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: exists() on keys true",
expression: "c.labels.exists(name, name == 'key2')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: exists() on keys false",
expression: "!c.labels.exists(name, name == 'key3')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: empty map size",
expression: "size(c.emptyMap) == 0",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: exists() on empty",
expression: "!c.emptyMap.exists(name, true)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: all() on empty",
expression: "c.emptyMap.all(name, false)",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: convert to map type",
expression: "type(c.labels) == map",
activation: map[string]typedValue{"c": complex1},
},
{
name: "map: convert map to type type",
expression: "type(c.labels) == map",
activation: map[string]typedValue{"c": complex1},
},
// Pointer Tests
{
name: "pointer: access through non-nil pointer field",
expression: "c.childPtr.s == 'world'",
activation: map[string]typedValue{"c": complex1},
},
{
name: "pointer: compare non-nil pointer field",
expression: "c.childPtr == s2",
activation: map[string]typedValue{
"c": complex1,
"s2": struct2,
},
},
{
name: "pointer: access through nil pointer field (error)",
expression: "c.nilPtr.s",
activation: map[string]typedValue{"c": complex1},
wantErr: "no such key: nilPtr", // Accessing field 's' on a null object
},
{
name: "pointer: check has() nil pointer",
expression: "!has(c.nilPtr)",
activation: map[string]typedValue{"c": complex1},
},
// Type Tests
{
name: "type: string",
expression: "type(obj.s) == string",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "type: int",
expression: "type(obj.i) == int",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "type: bool",
expression: "type(obj.b) == bool",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "type: float",
expression: "type(obj.f) == double",
activation: map[string]typedValue{"obj": struct2},
},
{
name: "type: slice",
expression: "type(c.tags) == list",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: map",
expression: "type(c.labels) == map",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: time",
expression: "type(c.time) == google.protobuf.Timestamp",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: microTime",
expression: "type(c.microTime) == google.protobuf.Timestamp",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: duration",
expression: "type(c.timeout) == google.protobuf.Duration",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: bytes",
expression: "type(c.rawBytes) == bytes",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: int32",
expression: "type(c.i32) == int",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: int64",
expression: "type(c.i64) == int",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: float32",
expression: "type(c.f32) == double",
activation: map[string]typedValue{"c": complex1},
},
{
name: "type: enum",
expression: "type(c.enum) == string",
activation: map[string]typedValue{"c": complex1},
},
// listType=map
{
name: "listType=map: equal",
expression: "c1.mapList == c2.mapList",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "listType=map: not equal",
expression: "c1.mapList != c3.mapList",
activation: map[string]typedValue{
"c1": complex1,
"c3": complex3,
},
},
{
name: "listType=map: add overlapping",
expression: "c1.mapList + c2.mapList == c1.mapList",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "listType=map: add non-overlapping",
expression: "c1.mapList + c3.mapList == c4.mapList",
activation: map[string]typedValue{
"c1": complex1,
"c3": complex3,
"c4": complex4,
},
},
// listType=set
{
name: "listType=set: equal",
expression: "c1.setList == c2.setList",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "listType=set: not equal",
expression: "c1.setList != c3.setList",
activation: map[string]typedValue{
"c1": complex1,
"c3": complex3,
},
},
{
name: "listType=set: add overlapping",
expression: "c1.setList + c2.setList == c1.setList",
activation: map[string]typedValue{
"c1": complex1,
"c2": complex2,
},
},
{
name: "listType=set: add non-overlapping",
expression: "c1.setList + c3.setList == c4.setList",
activation: map[string]typedValue{
"c1": complex1,
"c3": complex3,
"c4": complex4,
},
},
// Special K8s Types
{
name: "time: comparison equals",
expression: "c.time == timestamp('2000-01-01T12:00:00Z')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "microTime: comparison equals",
expression: "c.microTime == timestamp('2000-01-01T12:00:00.000001Z')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "duration: comparison equals",
expression: "c.timeout == duration('5s')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "duration: comparison greater",
expression: "c.timeout > duration('1s')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "intOrString: int comparison",
expression: "c.intOrString == 5",
activation: map[string]typedValue{"c": complex1},
},
{
name: "intOrString: string comparison",
expression: "c.intOrString == 'port'",
activation: map[string]typedValue{"c": complex2},
},
{
name: "quantity: comparison",
expression: "quantity(c.quantity).isGreaterThan(quantity('99m')) && quantity(c.quantity).isLessThan(quantity('101m'))",
activation: map[string]typedValue{"c": complex1},
},
{
name: "quantity: equality",
expression: "quantity(c.quantity) == quantity('100m')",
activation: map[string]typedValue{"c": complex1},
},
{
name: "bytes: size",
expression: "size(c.rawBytes) == 6",
activation: map[string]typedValue{"c": complex1},
},
{
name: "bytes: equality",
expression: "c.rawBytes == b'bytes1'",
activation: map[string]typedValue{"c": complex1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var opts []cel.EnvOption
for k := range tt.activation {
opts = append(opts, cel.Variable(k, cel.DynType))
}
opts = append(opts, cel.StdLib(), library.Quantity())
env, err := cel.NewEnv(opts...)
if err != nil {
t.Fatalf("Env creation error: %v", err)
}
t.Run("TypedToVal", func(t *testing.T) {
testTypedToVal(t, env, tt)
})
t.Run("UnstructuredToVal", func(t *testing.T) {
testUnstructuredToVal(t, tt, env)
})
})
}
}
func testTypedToVal(t *testing.T, env *cel.Env, tt testCase) {
typedOut, typedErr := evalExpression(t, env, tt.expression, typedToValActivation(tt.activation))
if typedErr != nil && len(tt.wantErr) == 0 {
t.Fatalf("Unexpected err with typed values: %v", typedErr)
}
if len(tt.wantErr) > 0 {
if typedErr == nil {
t.Fatalf("Expected error '%s' during evaluation with typed values, but got none", tt.wantErr)
}
if typedErr.Error() != tt.wantErr {
t.Fatalf("Expected error '%s' during evaluation with typed values, but got: %v", tt.wantErr, typedErr)
}
}
if len(tt.wantErr) == 0 && typedOut != types.True {
t.Errorf("Expected true with typed values but got %v", typedOut)
}
}
func testUnstructuredToVal(t *testing.T, tt testCase, env *cel.Env) {
a, err := unstructuredToValActivation(tt.activation)
if err != nil {
t.Fatalf("Unexpected error converting activation to unstructured: %v", err)
}
unstructuredOut, unstructuredErr := evalExpression(t, env, tt.expression, a)
if unstructuredErr != nil && len(tt.wantErr) == 0 {
t.Fatalf("Unexpected err with unstructured values: %v", unstructuredErr)
}
if len(tt.wantErr) > 0 {
if unstructuredErr == nil {
t.Fatalf("Expected error '%s' during evaluation with unstructured values, but got none", tt.wantErr)
}
if unstructuredErr.Error() != tt.wantErr {
t.Fatalf("Expected error '%s' during evaluation with unstructured values, but got: %v", tt.wantErr, unstructuredErr)
}
}
if len(tt.wantErr) == 0 && unstructuredOut != types.True {
t.Errorf("Expected true with unstructured values but got %v", unstructuredOut)
}
}
func evalExpression(t *testing.T, env *cel.Env, expression string, activation map[string]interface{}) (ref.Val, error) {
ast, iss := env.Compile(expression)
if iss.Err() != nil {
t.Fatalf("Compile error: %v :: %s", iss.Err(), expression)
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("Program error: %v :: %s", err, expression)
}
out, _, err := prg.Eval(activation)
return out, err
}
type Struct struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
S string `json:"s"`
I int `json:"i"`
B bool `json:"b"`
F float64 `json:"f"`
SP string `json:"sp"`
IP int `json:"ip"`
BP bool `json:"bp"`
FP float64 `json:"fp"`
}
type StructOmitEmpty struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
S string `json:"s,omitempty"`
I int `json:"i,omitempty"`
B bool `json:"b,omitempty"`
F float64 `json:"f,omitempty"`
SP string `json:"sp,omitempty"`
IP int `json:"ip,omitempty"`
BP bool `json:"bp,omitempty"`
FP float64 `json:"fp,omitempty"`
}
type StructOmitZero struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitzero"`
S string `json:"s,omitzero"`
I int `json:"i,omitzero"`
B bool `json:"b,omitzero"`
F float64 `json:"f,omitzero"`
SP string `json:"sp,omitzero"`
IP int `json:"ip,omitzero"`
BP bool `json:"bp,omitzero"`
FP float64 `json:"fp,omitzero"`
}
var structSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": *stringSchema,
"apiVersion": *stringSchema,
"metadata": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *stringSchema,
},
},
},
"s": *stringSchema,
"i": *int64Schema,
"b": *boolSchema,
"f": *float64Schema,
"so": *stringSchema,
"io": *int64Schema,
"bo": *boolSchema,
"fo": *float64Schema,
},
},
}
func (s Struct) GetObjectKind() schema.ObjectKind {
panic("not implemented")
}
func (s Struct) DeepCopyObject() runtime.Object {
panic("not implemented")
}
type Nested struct {
Name string `json:"name"`
Info Struct `json:"info"`
}
var nestedSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *stringSchema,
"info": *structSchema,
},
},
}
func (s Nested) GetObjectKind() schema.ObjectKind {
panic("not implemented")
}
func (s Nested) DeepCopyObject() runtime.Object {
panic("not implemented")
}
type Complex struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
ID string `json:"id"`
Tags []string `json:"tags"`
Labels map[string]string `json:"labels"`
NestedObj Nested `json:"nestedObj"`
NestedEmpty Nested `json:"nestedEmpty,omitempty"`
NestedZero Nested `json:"nestedZero,omitzero"`
Timeout metav1.Duration `json:"timeout"`
Time metav1.Time `json:"time"`
MicroTime metav1.MicroTime `json:"microTime"`
RawBytes []byte `json:"rawBytes"`
NilBytes []byte `json:"nilBytes"` // Always nil
ChildPtr *Struct `json:"childPtr"`
NilPtr *Struct `json:"nilPtr"` // Always nil
EmptySlice []int `json:"emptySlice"`
NilSlice []int `json:"nilSlice"` // Always nil
EmptyMap map[string]int `json:"emptyMap"`
NilMap map[string]int `json:"nilMap"` // Always nil
IntOrString intstr.IntOrString `json:"intOrString"`
Quantity resource.Quantity `json:"quantity"`
I32 int32 `json:"i32"`
I64 int64 `json:"i64"`
F32 float32 `json:"f32"`
Enum EnumType `json:"enum"`
MapList []MapListEntry `json:"mapList"`
SetList []SetEntry `json:"setList"`
}
type SetEntry int
type MapListEntry struct {
Key1 string `json:"key1"`
Key2 string `json:"key2"`
Value int `json:"value"`
}
var complexSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"kind": *stringSchema,
"apiVersion": *stringSchema,
"metadata": {
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *stringSchema,
},
},
},
"id": *stringSchema,
"tags": *stringArraySchema,
"labels": *stringMapSchema,
"nestedObj": *nestedSchema,
"nestedEmpty": *nestedSchema,
"nestedZero": *nestedSchema,
"timeout": durationFormat,
"time": timeFormat,
"microTime": timeFormat,
"rawBytes": bytesFormat,
"nilBytes": bytesFormat,
"childPtr": *structSchema,
"nilPtr": *structSchema,
"emptySlice": *intArraySchema,
"nilSlice": *intArraySchema,
"emptyMap": *intMapSchema,
"nilMap": *intMapSchema,
"intOrString": {
VendorExtensible: intOrStringSchema,
},
"quantity": *stringSchema, // TODO: If we add a quantity format to OpenAPI, test it here
"i32": *int32Schema,
"i64": *int64Schema,
"f32": *float32Schema,
"enum": *stringSchema,
"mapList": {
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
"x-kubernetes-list-type": "map",
"x-kubernetes-list-map-keys": []any{"key1", "key2"},
}},
SchemaProps: spec.SchemaProps{Type: []string{"array"}, Items: &spec.SchemaOrArray{
Schema: &spec.Schema{SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key1": *stringSchema,
"key2": *stringSchema,
"value": *int64Schema,
},
}},
}},
},
"setList": {
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
"x-kubernetes-list-type": "set",
}},
SchemaProps: intArraySchema.SchemaProps,
},
},
},
}
func (c Complex) GetObjectKind() schema.ObjectKind {
panic("not implemented")
}
func (c Complex) DeepCopyObject() runtime.Object {
panic("not implemented")
}
type EnumType string
const (
EnumTypeA EnumType = "a"
EnumTypeB EnumType = "b"
)
var (
stringSchema = spec.StringProperty()
bytesFormat = spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "byte"}}
durationFormat = spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "duration"}}
timeFormat = spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{"string"}, Format: "date-time"}}
intOrStringSchema = spec.VendorExtensible{Extensions: map[string]interface{}{"x-kubernetes-int-or-string": true}}
int32Schema = spec.Int32Property()
int64Schema = spec.Int64Property()
boolSchema = spec.BoolProperty()
float32Schema = spec.Float32Property()
float64Schema = spec.Float64Property()
stringArraySchema = spec.ArrayProperty(stringSchema)
intArraySchema = spec.ArrayProperty(int64Schema)
stringMapSchema = spec.MapProperty(stringSchema)
intMapSchema = spec.MapProperty(int64Schema)
)
func typedToValActivation(vals map[string]typedValue) map[string]interface{} {
activation := make(map[string]interface{}, len(vals))
for k, tv := range vals {
s := &openapi.Schema{Schema: tv.schema}
activation[k] = common.TypedToVal(tv.value, s)
}
return activation
}
// unstructuredToValActivation converts the values in the activation map to map[string]interface{}.
func unstructuredToValActivation(vals map[string]typedValue) (map[string]interface{}, error) {
activation := make(map[string]interface{}, len(vals))
for k, tv := range vals {
s := &openapi.Schema{Schema: tv.schema}
switch v := tv.value.(type) {
case runtime.Object:
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&v)
if err != nil {
return nil, err
}
activation[k] = common.UnstructuredToVal(u, s)
case *runtime.Object:
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(v)
if err != nil {
return nil, err
}
activation[k] = common.UnstructuredToVal(u, s)
default:
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&wrap{Value: &v})
if err != nil {
return nil, err
}
if uv, ok := u["value"]; ok && uv != nil {
activation[k] = common.UnstructuredToVal(uv, s)
} else {
activation[k] = types.NullValue
}
}
}
return activation, nil
}
type wrap struct {
Value any `json:"value"`
}
type typedValue struct {
value any
schema *spec.Schema
}
const RFC3339Micro = "2006-01-02T15:04:05.000000Z07:00"
const RFC3339 = "2006-01-02T15:04:05Z07:00"
type testCase struct {
name string
expression string
activation map[string]typedValue
wantErr string
}