diff --git a/go.mod b/go.mod index 955e8b05b..f6f069617 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 k8s.io/api v0.0.0-20250503031400-f7e72be095ee k8s.io/apimachinery v0.0.0-20250506191157-e07849993d77 - k8s.io/client-go v0.0.0-20250503031754-7aa362d5dd59 + k8s.io/client-go v0.0.0-20250506191944-2d0ad1ac54ea k8s.io/component-base v0.0.0-20250503032835-394b28e84ba2 k8s.io/klog/v2 v2.130.1 k8s.io/kms v0.0.0-20250401105328-de9f6e9dd930 diff --git a/go.sum b/go.sum index 9c082b0a7..d4d1badd4 100644 --- a/go.sum +++ b/go.sum @@ -371,8 +371,8 @@ k8s.io/api v0.0.0-20250503031400-f7e72be095ee h1:+YExLdNpiASfnQXQfpyLIGIps0RcJPN k8s.io/api v0.0.0-20250503031400-f7e72be095ee/go.mod h1:AsuSCzGYZszSLf5GB+qx8FBGGirk0I/TZUkQJFsPRAQ= k8s.io/apimachinery v0.0.0-20250506191157-e07849993d77 h1:bwypcAN4gpWZ8VRVrgr0tYmunGNRCD869R3/h5o2Qa4= k8s.io/apimachinery v0.0.0-20250506191157-e07849993d77/go.mod h1:b+h1nads2hmyfwvvorkgHUriRTTaJ2p2mk0l03sESn8= -k8s.io/client-go v0.0.0-20250503031754-7aa362d5dd59 h1:n8NUttTLfP515BaQfYTG+ktgOhXknisqBRg2ttvGMzM= -k8s.io/client-go v0.0.0-20250503031754-7aa362d5dd59/go.mod h1:M+V5ZD54hCEexMLVuebHThquNdpXZdNqpuDCoD1LjSM= +k8s.io/client-go v0.0.0-20250506191944-2d0ad1ac54ea h1:aMwjT5s9LQfXcA5Uwm4lqnMhm7i1dHxCdd48GwM7ed4= +k8s.io/client-go v0.0.0-20250506191944-2d0ad1ac54ea/go.mod h1:uX91j8XTGmdZmKa16H99QJ5W5lFV657dfka2v4AHS74= k8s.io/component-base v0.0.0-20250503032835-394b28e84ba2 h1:Sn22KR7kHIivpSgJYqNzt5d2mBYBipLAXLK02SlVqHM= k8s.io/component-base v0.0.0-20250503032835-394b28e84ba2/go.mod h1:WgzO+P6/OI1nAOmj6BPnmaioKc8o9E9RdXNOCfIOV4U= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/pkg/cel/common/values_test.go b/pkg/cel/common/values_test.go index 7acfe6963..04216efa0 100644 --- a/pkg/cel/common/values_test.go +++ b/pkg/cel/common/values_test.go @@ -41,6 +41,10 @@ import ( 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} @@ -70,6 +74,8 @@ func TestToValue(t *testing.T) { 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, @@ -110,6 +116,8 @@ func TestToValue(t *testing.T) { 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 @@ -291,10 +299,50 @@ func TestToValue(t *testing.T) { activation: map[string]typedValue{"c": complex1}, }, { - name: "struct: embedded struct: omitempty struct field", - expression: "!has(c.metadata.labels)", + 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 { @@ -1023,23 +1071,63 @@ func evalExpression(t *testing.T, env *cel.Env, expression string, activation ma type Struct struct { metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` + metav1.ObjectMeta `json:"metadata"` S string `json:"s"` I int `json:"i"` B bool `json:"b"` F float64 `json:"f"` - SO string `json:"so,omitempty"` - IO int `json:"io,omitempty"` - BO bool `json:"bo,omitempty"` - FO float64 `json:"fo,omitempty"` + 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, @@ -1091,6 +1179,8 @@ type Complex struct { 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"` @@ -1134,21 +1224,23 @@ var complexSchema = &spec.Schema{ }, }, }, - "id": *stringSchema, - "tags": *stringArraySchema, - "labels": *stringMapSchema, - "nestedObj": *nestedSchema, - "timeout": durationFormat, - "time": timeFormat, - "microTime": timeFormat, - "rawBytes": bytesFormat, - "nilBytes": bytesFormat, - "childPtr": *structSchema, - "nilPtr": *structSchema, - "emptySlice": *intArraySchema, - "nilSlice": *intArraySchema, - "emptyMap": *intMapSchema, - "nilMap": *intMapSchema, + "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, },