diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go index beeb181e509..def5e025f8b 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/celcoststability_test.go @@ -230,8 +230,8 @@ func TestCelCostStability(t *testing.T) { }, }, {name: "listSets", - obj: objs([]interface{}{"a", "b", "c"}, []interface{}{"a", "c", "b"}), - schema: schemas(listSetType(&stringType), listSetType(&stringType)), + obj: objs([]interface{}{"a", "b", "c"}, []interface{}{"a", "c", "b"}, buildLargeArray(1000)), + schema: schemas(listSetType(&stringType), listSetType(&stringType), listSetType(&integerType)), expectCost: map[string]int64{ // equal even though order is different "self.val1 == ['c', 'b', 'a']": 3, @@ -241,6 +241,12 @@ func TestCelCostStability(t *testing.T) { "!('x' in self.val1)": 6, "self.val1 + self.val2 == ['a', 'b', 'c']": 6, "self.val1 + ['c', 'd'] == ['a', 'b', 'c', 'd']": 4, + "sets.contains(self.val1, ['a'])": 6, + "sets.equivalent(self.val1, ['a', 'b', 'c'])": 21, + "sets.intersects(self.val1, ['a'])": 6, + "sets.contains(self.val3, [1])": 1003, + "!sets.equivalent(self.val3, [1, 2, 3])": 6004, + "sets.intersects(self.val3, [1])": 1003, }, }, {name: "listMaps", @@ -1157,6 +1163,14 @@ func TestCelCostStability(t *testing.T) { } } +func buildLargeArray(size int) []interface{} { + lArray := make([]interface{}, size) + for i := 0; i < len(lArray); i++ { + lArray[i] = i + } + return lArray +} + func TestCelEstimatedCostStability(t *testing.T) { cases := []struct { name string diff --git a/staging/src/k8s.io/apiserver/pkg/cel/environment/base.go b/staging/src/k8s.io/apiserver/pkg/cel/environment/base.go index 22211812147..76a0bccee8a 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/environment/base.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/environment/base.go @@ -22,7 +22,9 @@ import ( "sync" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/ext" + "github.com/google/cel-go/interpreter" "golang.org/x/sync/singleflight" "k8s.io/apimachinery/pkg/util/version" @@ -106,6 +108,21 @@ var baseOpts = []VersionedOptions{ ext.Strings(ext.StringsVersion(2)), }, }, + // Set library + { + IntroducedVersion: version.MajorMinor(1, 29), + EnvOptions: []cel.EnvOption{ + ext.Sets(), + // cel-go v0.17.7 introduced CostEstimatorOptions. + // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. + cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)), + }, + ProgramOptions: []cel.ProgramOption{ + // cel-go v0.17.7 introduced CostTrackerOptions. + // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. + cel.CostTrackerOptions(interpreter.PresenceTestHasCost(false)), + }, + }, } // MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go index 89768812d23..efe8b42d425 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go @@ -527,6 +527,236 @@ func TestQuantityCost(t *testing.T) { } } +func TestSetsCost(t *testing.T) { + cases := []struct { + name string + expr string + expectEstimatedCost checker.CostEstimate + expectRuntimeCost uint64 + }{ + { + name: "sets", + expr: `sets.contains([], [])`, + expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, + expectRuntimeCost: 21, + }, + { + expr: `sets.contains([1], [])`, + expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, + expectRuntimeCost: 21, + }, + { + expr: `sets.contains([1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, + expectRuntimeCost: 22, + }, + { + expr: `sets.contains([1], [1, 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.contains([1, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.contains([2, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.contains([1, 2, 3, 4], [2, 3])`, + expectEstimatedCost: checker.CostEstimate{Min: 29, Max: 29}, + expectRuntimeCost: 29, + }, + { + expr: `sets.contains([1], [1.0, 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.contains([1, 2], [2u, 2.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.contains([1, 2u], [2, 2.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.contains([1, 2.0, 3u], [1.0, 2u, 3])`, + expectEstimatedCost: checker.CostEstimate{Min: 30, Max: 30}, + expectRuntimeCost: 30, + }, + { + expr: `sets.contains([[1], [2, 3]], [[2, 3.0]])`, + // 10 for each list creation, top-level list sizes are 2, 1 + expectEstimatedCost: checker.CostEstimate{Min: 53, Max: 53}, + expectRuntimeCost: 53, + }, + { + expr: `!sets.contains([1], [2])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `!sets.contains([1], [1, 2])`, + expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, + expectRuntimeCost: 24, + }, + { + expr: `!sets.contains([1], ["1", 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, + expectRuntimeCost: 24, + }, + { + expr: `!sets.contains([1], [1.1, 1u])`, + expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, + expectRuntimeCost: 24, + }, + + // set equivalence (note the cost factor is higher as it's basically two contains checks) + { + expr: `sets.equivalent([], [])`, + expectEstimatedCost: checker.CostEstimate{Min: 21, Max: 21}, + expectRuntimeCost: 21, + }, + { + expr: `sets.equivalent([1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.equivalent([1], [1, 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.equivalent([1, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.equivalent([1], [1u, 1.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.equivalent([1], [1u, 1.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 25, Max: 25}, + expectRuntimeCost: 25, + }, + { + expr: `sets.equivalent([1, 2, 3], [3u, 2.0, 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 39, Max: 39}, + expectRuntimeCost: 39, + }, + { + expr: `sets.equivalent([[1.0], [2, 3]], [[1], [2, 3.0]])`, + expectEstimatedCost: checker.CostEstimate{Min: 69, Max: 69}, + expectRuntimeCost: 69, + }, + { + expr: `!sets.equivalent([2, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26}, + expectRuntimeCost: 26, + }, + { + expr: `!sets.equivalent([1], [1, 2])`, + expectEstimatedCost: checker.CostEstimate{Min: 26, Max: 26}, + expectRuntimeCost: 26, + }, + { + expr: `!sets.equivalent([1, 2], [2u, 2, 2.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, + expectRuntimeCost: 34, + }, + { + expr: `!sets.equivalent([1, 2], [1u, 2, 2.3])`, + expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34}, + expectRuntimeCost: 34, + }, + { + expr: `sets.intersects([1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, + expectRuntimeCost: 22, + }, + { + expr: `sets.intersects([1], [1, 1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.intersects([1, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.intersects([2, 1], [1])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.intersects([1], [1, 2])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.intersects([1], [1.0, 2])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `sets.intersects([1, 2], [2u, 2, 2.0])`, + expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27}, + expectRuntimeCost: 27, + }, + { + expr: `sets.intersects([1, 2], [1u, 2, 2.3])`, + expectEstimatedCost: checker.CostEstimate{Min: 27, Max: 27}, + expectRuntimeCost: 27, + }, + { + expr: `sets.intersects([[1], [2, 3]], [[1, 2], [2, 3.0]])`, + expectEstimatedCost: checker.CostEstimate{Min: 65, Max: 65}, + expectRuntimeCost: 65, + }, + { + expr: `!sets.intersects([], [])`, + expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, + expectRuntimeCost: 22, + }, + { + expr: `!sets.intersects([1], [])`, + expectEstimatedCost: checker.CostEstimate{Min: 22, Max: 22}, + expectRuntimeCost: 22, + }, + { + expr: `!sets.intersects([1], [2])`, + expectEstimatedCost: checker.CostEstimate{Min: 23, Max: 23}, + expectRuntimeCost: 23, + }, + { + expr: `!sets.intersects([1], ["1", 2])`, + expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, + expectRuntimeCost: 24, + }, + { + expr: `!sets.intersects([1], [1.1, 2u])`, + expectEstimatedCost: checker.CostEstimate{Min: 24, Max: 24}, + expectRuntimeCost: 24, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) + }) + } +} + func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) { est := &CostEstimator{SizeEstimator: &testCostEstimator{}} env, err := cel.NewEnv( @@ -536,6 +766,10 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate Lists(), Authz(), Quantity(), + ext.Sets(), + // cel-go v0.17.7 introduced CostEstimatorOptions. + // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. + cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)), ) if err != nil { t.Fatalf("%v", err)