mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 12:07:47 +00:00
Merge pull request #121455 from cici37/test_cost
CRD validation rule: Add stability tests for CEL cost estimation
This commit is contained in:
commit
03ba7efb85
@ -116,6 +116,11 @@ func TestCelCostStability(t *testing.T) {
|
||||
"self.val1.upperAscii() == 'ROOK TAKES 👑'": 6,
|
||||
"self.val1.lowerAscii() == 'rook takes 👑'": 6,
|
||||
"self.val1.lowerAscii() == self.val1.lowerAscii()": 10,
|
||||
// strings version 2
|
||||
"'%d %s %f %s %s'.format([1, 'abc', 1.0, duration('1m'), timestamp('2000-01-01T00:00:00.000Z')]) == '1 abc 1.000000 60s 2000-01-01T00:00:00Z'": 6,
|
||||
"'%e'.format([3.14]) == '3.140000 × 10⁰⁰'": 3,
|
||||
"'%o %o %o'.format([7, 8, 9]) == '7 10 11'": 2,
|
||||
"'%b %b %b'.format([7, 8, 9]) == '111 1000 1001'": 3,
|
||||
},
|
||||
},
|
||||
{name: "escaped strings",
|
||||
@ -1151,3 +1156,792 @@ func TestCelCostStability(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCelEstimatedCostStability(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
schema *schema.Structural
|
||||
expectCost map[string]uint64
|
||||
}{
|
||||
{name: "integers",
|
||||
// 1st obj and schema args are for "self.val1" field, 2nd for "self.val2" and so on.
|
||||
schema: schemas(integerType, integerType, int32Type, int32Type, int64Type, int64Type),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%d", math.MaxInt64)): 8,
|
||||
"self.val1 == self.val6": 4, // integer with no format is the same as int64
|
||||
"type(self.val1) == int": 4,
|
||||
fmt.Sprintf("self.val3 + 1 == %d + 1", math.MaxInt32): 5, // CEL integers are 64 bit
|
||||
},
|
||||
},
|
||||
{name: "numbers",
|
||||
schema: schemas(numberType, numberType, floatType, floatType, doubleType, doubleType, doubleType),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%f", math.MaxFloat64)): 8,
|
||||
"self.val1 == self.val6": 4, // number with no format is the same as float64
|
||||
"type(self.val1) == double": 4,
|
||||
|
||||
// Use a int64 value with a number openAPI schema type since float representations of whole numbers
|
||||
// (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml to json translation, and
|
||||
// then get parsed as int64s.
|
||||
"type(self.val7) == double": 4,
|
||||
"self.val7 == 1.0": 2,
|
||||
},
|
||||
},
|
||||
{name: "numeric comparisons",
|
||||
schema: schemas(integerType, numberType, floatType, doubleType, numberType, floatType, doubleType),
|
||||
expectCost: map[string]uint64{
|
||||
// xref: https://github.com/google/cel-spec/wiki/proposal-210
|
||||
|
||||
// compare integers with all float types
|
||||
"double(self.val1) < self.val4": 6,
|
||||
"self.val1 < int(self.val4)": 6,
|
||||
"double(self.val1) < self.val5": 6,
|
||||
"self.val1 < int(self.val5)": 6,
|
||||
"double(self.val1) < self.val6": 6,
|
||||
"self.val1 < int(self.val6)": 6,
|
||||
|
||||
// compare literal integers and floats
|
||||
"double(5) < 10.0": 2,
|
||||
"5 < int(10.0)": 2,
|
||||
|
||||
// compare integers with literal floats
|
||||
"double(self.val1) < 10.0": 4,
|
||||
},
|
||||
},
|
||||
{name: "unicode strings",
|
||||
schema: schemas(stringType, stringType),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'Rook takes 👑'"): 314585,
|
||||
"self.val1.startsWith('Rook')": 3,
|
||||
"!self.val1.startsWith('knight')": 4,
|
||||
"self.val1.matches('^[^0-9]*$')": 943721,
|
||||
"!self.val1.matches('^[0-9]*$')": 629149,
|
||||
"type(self.val1) == string": 4,
|
||||
"size(self.val1) == 12": 4,
|
||||
|
||||
// string functions (https://github.com/google/cel-go/blob/v0.9.0/ext/strings.go)
|
||||
"self.val1.charAt(3) == 'k'": 4,
|
||||
"self.val1.indexOf('o') == 1": 314576,
|
||||
"self.val1.indexOf('o', 2) == 2": 314576,
|
||||
"self.val1.replace(' ', 'x') == 'Rookxtakesx👑'": 629150,
|
||||
"self.val1.replace(' ', 'x', 1) == 'Rookxtakes 👑'": 629150,
|
||||
"self.val1.split(' ') == ['Rook', 'takes', '👑']": 629159,
|
||||
"self.val1.split(' ', 2) == ['Rook', 'takes 👑']": 629159,
|
||||
"self.val1.substring(5) == 'takes 👑'": 314576,
|
||||
"self.val1.substring(0, 4) == 'Rook'": 314576,
|
||||
"self.val1.substring(4, 10).trim() == 'takes'": 629149,
|
||||
"self.val1.upperAscii() == 'ROOK TAKES 👑'": 314577,
|
||||
"self.val1.lowerAscii() == 'rook takes 👑'": 314577,
|
||||
"self.val1.lowerAscii() == self.val1.lowerAscii()": 943723,
|
||||
},
|
||||
},
|
||||
{name: "escaped strings",
|
||||
schema: schemas(stringType, stringType),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'l1\\nl2'"): 314583,
|
||||
"self.val1 == '''l1\nl2'''": 3,
|
||||
},
|
||||
},
|
||||
{name: "bytes",
|
||||
schema: schemas(byteType, byteType),
|
||||
expectCost: map[string]uint64{
|
||||
"self.val1 == self.val2": 314577,
|
||||
"self.val1 == b'AB'": 3,
|
||||
"type(self.val1) == bytes": 4,
|
||||
"size(self.val1) == 2": 4,
|
||||
},
|
||||
},
|
||||
{name: "booleans",
|
||||
schema: schemas(booleanType, booleanType, booleanType, booleanType),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "true"): 8,
|
||||
"self.val1 != self.val4": 4,
|
||||
"type(self.val1) == bool": 4,
|
||||
},
|
||||
},
|
||||
{name: "duration format",
|
||||
schema: schemas(durationFormat, durationFormat),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "duration('1h2m3s4ms')"): 16,
|
||||
"self.val1 == duration('1h2m') + duration('3s4ms')": 6,
|
||||
"self.val1.getHours() == 1": 4,
|
||||
"type(self.val1) == google.protobuf.Duration": 4,
|
||||
},
|
||||
},
|
||||
{name: "date format",
|
||||
schema: schemas(dateFormat, dateFormat),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('1997-07-16T00:00:00.000Z')"): 14,
|
||||
"self.val1.getDate() == 16": 4,
|
||||
"type(self.val1) == google.protobuf.Timestamp": 4,
|
||||
},
|
||||
},
|
||||
{name: "date-time format",
|
||||
schema: schemas(dateTimeFormat, dateTimeFormat),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('2011-08-18T19:03:37.010+01:00')"): 16,
|
||||
"self.val1 == timestamp('2011-08-18T00:00:00.000+01:00') + duration('19h3m37s10ms')": 6,
|
||||
"self.val1.getDate('01:00') == 18": 4,
|
||||
"type(self.val1) == google.protobuf.Timestamp": 4,
|
||||
},
|
||||
},
|
||||
{name: "enums",
|
||||
schema: objectTypePtr(map[string]schema.Structural{"enumStr": {
|
||||
Generic: schema.Generic{
|
||||
Type: "string",
|
||||
},
|
||||
ValueValidation: &schema.ValueValidation{
|
||||
Enum: []schema.JSON{
|
||||
{Object: "Pending"},
|
||||
{Object: "Available"},
|
||||
{Object: "Bound"},
|
||||
{Object: "Released"},
|
||||
{Object: "Failed"},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.enumStr == 'Pending'": 3,
|
||||
"self.enumStr in ['Pending', 'Available']": 14,
|
||||
},
|
||||
},
|
||||
{name: "conversions",
|
||||
schema: schemas(integerType, numberType, numberType, numberType, booleanType, stringType, byteType, stringType, durationFormat, stringType, dateTimeFormat, stringType, dateFormat),
|
||||
expectCost: map[string]uint64{
|
||||
"int(self.val2) == self.val1": 5,
|
||||
"double(self.val1) == self.val2": 5,
|
||||
"bytes(self.val6) == self.val7": 629150,
|
||||
"string(self.val1) == self.val6": 314578,
|
||||
"string(self.val4) == '10.5'": 4,
|
||||
"string(self.val7) == self.val6": 629150,
|
||||
"duration(self.val8) == self.val9": 6,
|
||||
"timestamp(self.val10) == self.val11": 6,
|
||||
"string(self.val11) == self.val10": 314578,
|
||||
"timestamp(self.val12) == self.val13": 6,
|
||||
"string(self.val13) == self.val12": 314578,
|
||||
},
|
||||
},
|
||||
{name: "lists",
|
||||
schema: schemas(listType(&integerType), listType(&integerType)),
|
||||
expectCost: map[string]uint64{
|
||||
ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "[1, 2, 3]"): 157317,
|
||||
"1 in self.val1": 1572865,
|
||||
"self.val2[0] in self.val1": 1572868,
|
||||
"!(0 in self.val1)": 1572866,
|
||||
"self.val1 + self.val2 == [1, 2, 3, 1, 2, 3]": 16,
|
||||
"self.val1 + [4, 5] == [1, 2, 3, 4, 5]": 24,
|
||||
"has(self.val1)": 2,
|
||||
"has(self.val1) && has(self.val2)": 4,
|
||||
"!has(self.val1)": 3,
|
||||
"self.val1.all(k, size(self.val1) > 0)": 11010044,
|
||||
"self.val1.exists_one(k, self.val1 == [2])": 23592949,
|
||||
"!self.val1.exists_one(k, size(self.val1) > 0)": 9437183,
|
||||
"size(self.val1) == 2": 4,
|
||||
"size(self.val1.filter(k, size(self.val1) > 1)) == 1": 26738686,
|
||||
},
|
||||
},
|
||||
{name: "listSets",
|
||||
schema: schemas(listSetType(&stringType), listSetType(&stringType)),
|
||||
expectCost: map[string]uint64{
|
||||
// equal even though order is different
|
||||
"self.val1 == ['c', 'b', 'a']": 13,
|
||||
"self.val1 == self.val2": 104862,
|
||||
"'a' in self.val1": 1048577,
|
||||
"self.val2[0] in self.val1": 1048580,
|
||||
"!('x' in self.val1)": 1048578,
|
||||
"self.val1 + self.val2 == ['a', 'b', 'c']": 16,
|
||||
"self.val1 + ['c', 'd'] == ['a', 'b', 'c', 'd']": 24,
|
||||
"has(self.val1)": 2,
|
||||
"has(self.val1) && has(self.val2)": 4,
|
||||
"!has(self.val1)": 3,
|
||||
"self.val1.all(k, size(self.val1) > 0)": 7340028,
|
||||
"self.val1.exists_one(k, self.val1 == ['a'])": 15728629,
|
||||
"!self.val1.exists_one(k, size(self.val1) > 0)": 6291455,
|
||||
"size(self.val1) == 2": 4,
|
||||
"size(self.val1.filter(k, size(self.val1) > 1)) == 1": 17825790,
|
||||
},
|
||||
},
|
||||
{name: "listMaps",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"objs": listType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
|
||||
"k": stringType,
|
||||
"v": stringType,
|
||||
}))),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.objs[0] == self.objs[1]": 104864, // equal even though order is different
|
||||
"self.objs[0] + self.objs[2] == self.objs[2]": 104868, // rhs overwrites lhs values
|
||||
"self.objs[2] + self.objs[0] == self.objs[0]": 104868,
|
||||
|
||||
"self.objs[0] == [self.objs[0][0], self.objs[0][1]]": 22, // equal against a declared list
|
||||
"self.objs[0] == [self.objs[0][1], self.objs[0][0]]": 22,
|
||||
|
||||
"self.objs[2] + [self.objs[0][0], self.objs[0][1]] == self.objs[0]": 104883, // concat against a declared list
|
||||
"size(self.objs[0] + [self.objs[3][0]]) == 3": 20,
|
||||
"has(self.objs)": 2,
|
||||
"has(self.objs) && has(self.objs)": 4,
|
||||
"!has(self.objs)": 3,
|
||||
"self.objs[0].all(k, size(self.objs[0]) > 0)": 8388604,
|
||||
"self.objs[0].exists_one(k, size(self.objs[0]) > 0)": 7340030,
|
||||
"!self.objs[0].exists_one(k, size(self.objs[0]) > 0)": 7340031,
|
||||
"size(self.objs[0]) == 2": 5,
|
||||
"size(self.objs[0].filter(k, size(self.objs[0]) > 1)) == 1": 18874366,
|
||||
},
|
||||
},
|
||||
{name: "maps",
|
||||
schema: schemas(mapType(&stringType), mapType(&stringType)),
|
||||
expectCost: map[string]uint64{
|
||||
"self.val1 == self.val2": 39326, // equal even though order is different
|
||||
"'k1' in self.val1": 3,
|
||||
"!('k3' in self.val1)": 4,
|
||||
"self.val1 == {'k1': 'a', 'k2': 'b'}": 33,
|
||||
"has(self.val1)": 2,
|
||||
"has(self.val1) && has(self.val2)": 4,
|
||||
"!has(self.val1)": 3,
|
||||
"self.val1.all(k, size(self.val1) > 0)": 2752508,
|
||||
"self.val1.exists_one(k, size(self.val1) > 0)": 2359294,
|
||||
"!self.val1.exists_one(k, size(self.val1) > 0)": 2359295,
|
||||
"size(self.val1) == 2": 4,
|
||||
"size(self.val1.filter(k, size(self.val1) > 1)) == 1": 6684670,
|
||||
},
|
||||
},
|
||||
{name: "objects",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"objs": listType(objectTypePtr(map[string]schema.Structural{
|
||||
"f1": stringType,
|
||||
"f2": stringType,
|
||||
})),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.objs[0] == self.objs[1]": 6,
|
||||
},
|
||||
},
|
||||
{name: "object access",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"a": objectType(map[string]schema.Structural{
|
||||
"b": integerType,
|
||||
"c": integerType,
|
||||
"d": withNullable(true, integerType),
|
||||
}),
|
||||
"a1": objectType(map[string]schema.Structural{
|
||||
"b1": objectType(map[string]schema.Structural{
|
||||
"c1": integerType,
|
||||
}),
|
||||
"d2": objectType(map[string]schema.Structural{
|
||||
"e2": integerType,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#field-selection
|
||||
expectCost: map[string]uint64{
|
||||
"has(self.a.b)": 3,
|
||||
"has(self.a1.b1.c1)": 4,
|
||||
"!(has(self.a1.d2) && has(self.a1.d2.e2))": 8, // must check intermediate optional fields (see below no such key error for d2)
|
||||
"!has(self.a1.d2)": 4,
|
||||
"has(self.a)": 2,
|
||||
"has(self.a) && has(self.a1)": 4,
|
||||
"!has(self.a)": 3,
|
||||
},
|
||||
},
|
||||
{name: "map access",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"val": mapType(&integerType),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// idiomatic map access
|
||||
"!('a' in self.val)": 4,
|
||||
"'b' in self.val": 3,
|
||||
"!('c' in self.val)": 4,
|
||||
"'d' in self.val": 3,
|
||||
// field selection also possible if map key is a valid CEL identifier
|
||||
"!has(self.val.a)": 4,
|
||||
"has(self.val.b)": 3,
|
||||
"!has(self.val.c)": 4,
|
||||
"has(self.val.d)": 3,
|
||||
"self.val.all(k, self.val[k] > 0)": 3595115,
|
||||
"self.val.exists_one(k, self.val[k] == 2)": 2696338,
|
||||
"!self.val.exists_one(k, self.val[k] > 0)": 3145728,
|
||||
"size(self.val) == 2": 4,
|
||||
"size(self.val.filter(k, self.val[k] > 1)) == 1": 8089017,
|
||||
},
|
||||
},
|
||||
{name: "listMap access",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"listMap": listMapType([]string{"k"}, objectTypePtr(map[string]schema.Structural{
|
||||
"k": stringType,
|
||||
"v": stringType,
|
||||
"v2": stringType,
|
||||
})),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"has(self.listMap[0].v)": 4,
|
||||
"self.listMap.all(m, m.k.startsWith('a'))": 6291453,
|
||||
"self.listMap.all(m, !has(m.v2) || m.v2 == 'z')": 9437178,
|
||||
"self.listMap.exists(m, m.k.endsWith('1'))": 7340028,
|
||||
"self.listMap.exists_one(m, m.k == 'a3')": 5242879,
|
||||
"!self.listMap.all(m, m.k.endsWith('1'))": 6291454,
|
||||
"!self.listMap.exists(m, m.v == 'x')": 7340029,
|
||||
"!self.listMap.exists_one(m, m.k.startsWith('a'))": 5242880,
|
||||
"size(self.listMap.filter(m, m.k == 'a1')) == 1": 16777215,
|
||||
"self.listMap.exists(m, m.k == 'a1' && m.v == 'b1')": 10485753,
|
||||
"self.listMap.map(m, m.v).exists(v, v == 'b1')": uint64(18446744073709551615),
|
||||
|
||||
// test comprehensions where the field used in predicates is unset on all but one of the elements:
|
||||
// - with has checks:
|
||||
|
||||
"self.listMap.exists(m, has(m.v2) && m.v2 == 'z')": 9437178,
|
||||
"!self.listMap.all(m, has(m.v2) && m.v2 != 'z')": 8388604,
|
||||
"self.listMap.exists_one(m, has(m.v2) && m.v2 == 'z')": 7340029,
|
||||
"self.listMap.filter(m, has(m.v2) && m.v2 == 'z').size() == 1": 18874365,
|
||||
// undocumented overload of map that takes a filter argument. This is the same as .filter().map()
|
||||
"self.listMap.map(m, has(m.v2) && m.v2 == 'z', m.v2).size() == 1": 19922940,
|
||||
"self.listMap.filter(m, has(m.v2) && m.v2 == 'z').map(m, m.v2).size() == 1": uint64(18446744073709551615),
|
||||
// - without has checks:
|
||||
|
||||
// all() and exists() macros ignore errors from predicates so long as the condition holds for at least one element
|
||||
"self.listMap.exists(m, m.v2 == 'z')": 7340028,
|
||||
"!self.listMap.all(m, m.v2 != 'z')": 6291454,
|
||||
},
|
||||
},
|
||||
{name: "list access",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"array": listType(&integerType),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"2 in self.array": 1572865,
|
||||
"self.array.all(e, e > 0)": 7864318,
|
||||
"self.array.exists(e, e > 2)": 9437181,
|
||||
"self.array.exists_one(e, e > 4)": 6291456,
|
||||
"!self.array.all(e, e < 2)": 7864319,
|
||||
"!self.array.exists(e, e < 0)": 9437182,
|
||||
"!self.array.exists_one(e, e == 2)": 4718594,
|
||||
"self.array.all(e, e < 100)": 7864318,
|
||||
"size(self.array.filter(e, e%2 == 0)) == 3": 25165823,
|
||||
"self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)": uint64(18446744073709551615),
|
||||
"size(self.array) == 8": 4,
|
||||
},
|
||||
},
|
||||
{name: "listSet access",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"set": listType(&integerType),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"3 in self.set": 1572865,
|
||||
"self.set.all(e, e > 0)": 7864318,
|
||||
"self.set.exists(e, e > 3)": 9437181,
|
||||
"self.set.exists_one(e, e == 3)": 4718593,
|
||||
"!self.set.all(e, e < 3)": 7864319,
|
||||
"!self.set.exists(e, e < 0)": 9437182,
|
||||
"!self.set.exists_one(e, e > 3)": 6291457,
|
||||
"self.set.all(e, e < 10)": 7864318,
|
||||
"size(self.set.filter(e, e%2 == 0)) == 2": 25165823,
|
||||
"self.set.map(e, e * 20).filter(e, e > 50).exists_one(e, e == 60)": uint64(18446744073709551615),
|
||||
"size(self.set) == 5": 4,
|
||||
},
|
||||
},
|
||||
{name: "typemeta and objectmeta access specified",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"kind": stringType,
|
||||
"apiVersion": stringType,
|
||||
"metadata": objectType(map[string]schema.Structural{
|
||||
"name": stringType,
|
||||
"generateName": stringType,
|
||||
}),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.kind == 'Pod'": 3,
|
||||
"self.apiVersion == 'v1'": 3,
|
||||
"self.metadata.name == 'foo'": 4,
|
||||
"self.metadata.generateName == 'pickItForMe'": 5,
|
||||
},
|
||||
},
|
||||
|
||||
// Kubernetes special types
|
||||
{name: "embedded object",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"embedded": {
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XEmbeddedResource: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are always accessible
|
||||
// even if not specified in the schema.
|
||||
"self.embedded.kind == 'Pod'": 4,
|
||||
"self.embedded.apiVersion == 'v1'": 4,
|
||||
"self.embedded.metadata.name == 'foo'": 5,
|
||||
"self.embedded.metadata.generateName == 'pickItForMe'": 6,
|
||||
},
|
||||
},
|
||||
{name: "embedded object with properties",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"embedded": {
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XEmbeddedResource: true,
|
||||
},
|
||||
Properties: map[string]schema.Structural{
|
||||
"kind": stringType,
|
||||
"apiVersion": stringType,
|
||||
"metadata": objectType(map[string]schema.Structural{
|
||||
"name": stringType,
|
||||
"generateName": stringType,
|
||||
}),
|
||||
"spec": objectType(map[string]schema.Structural{
|
||||
"field1": stringType,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// in this case 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are specified in the
|
||||
// schema, but they would be accessible even if they were not
|
||||
"self.embedded.kind == 'Pod'": 4,
|
||||
"self.embedded.apiVersion == 'v1'": 4,
|
||||
"self.embedded.metadata.name == 'foo'": 5,
|
||||
"self.embedded.metadata.generateName == 'pickItForMe'": 6,
|
||||
// the specified embedded fields are accessible
|
||||
"self.embedded.spec.field1 == 'a'": 5,
|
||||
},
|
||||
},
|
||||
{name: "embedded object with preserve unknown",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"embedded": {
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
XEmbeddedResource: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// 'kind', 'apiVersion', 'metadata.name' and 'metadata.generateName' are always accessible
|
||||
// even if not specified in the schema, regardless of if x-kubernetes-preserve-unknown-fields is set.
|
||||
"self.embedded.kind == 'Pod'": 4,
|
||||
"self.embedded.apiVersion == 'v1'": 4,
|
||||
"self.embedded.metadata.name == 'foo'": 5,
|
||||
"self.embedded.metadata.generateName == 'pickItForMe'": 6,
|
||||
|
||||
// the object exists
|
||||
"has(self.embedded)": 2,
|
||||
},
|
||||
},
|
||||
{name: "string in intOrString",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"something": intOrStringType(),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// typical int-or-string usage would be to check both types
|
||||
"type(self.something) == int ? self.something == 1 : self.something == '25%'": 7,
|
||||
// to require the value be a particular type, guard it with a runtime type check
|
||||
"type(self.something) == string && self.something == '25%'": 7,
|
||||
|
||||
// In Kubernetes 1.24 and later, the CEL type returns false for an int-or-string comparison against the
|
||||
// other type, making it safe to write validation rules like:
|
||||
"self.something == '25%'": 3,
|
||||
"self.something != 1": 3,
|
||||
"self.something == 1 || self.something == '25%'": 6,
|
||||
"self.something == '25%' || self.something == 1": 6,
|
||||
|
||||
// Because the type is dynamic it receives no type checking, and evaluates to false when compared to
|
||||
// other types at runtime.
|
||||
"self.something != ['anything']": 13,
|
||||
},
|
||||
},
|
||||
{name: "int in intOrString",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"something": intOrStringType(),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// typical int-or-string usage would be to check both types
|
||||
"type(self.something) == int ? self.something == 1 : self.something == '25%'": 7,
|
||||
// to require the value be a particular type, guard it with a runtime type check
|
||||
"type(self.something) == int && self.something == 1": 7,
|
||||
|
||||
// In Kubernetes 1.24 and later, the CEL type returns false for an int-or-string comparison against the
|
||||
// other type, making it safe to write validation rules like:
|
||||
"self.something == 1": 3,
|
||||
"self.something != 'some string'": 4,
|
||||
"self.something == 1 || self.something == '25%'": 6,
|
||||
"self.something == '25%' || self.something == 1": 6,
|
||||
|
||||
// Because the type is dynamic it receives no type checking, and evaluates to false when compared to
|
||||
// other types at runtime.
|
||||
"self.something != ['anything']": 13,
|
||||
},
|
||||
},
|
||||
{name: "null in intOrString",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"something": withNullable(true, intOrStringType()),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"!has(self.something)": 3,
|
||||
},
|
||||
},
|
||||
{name: "percent comparison using intOrString",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"min": intOrStringType(),
|
||||
"current": integerType,
|
||||
"available": integerType,
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// validate that if 'min' is a string that it is a percentage
|
||||
`type(self.min) == string && self.min.matches(r'(\d+(\.\d+)?%)')`: 1258298,
|
||||
// validate that 'min' can be either a exact value minimum, or a minimum as a percentage of 'available'
|
||||
"type(self.min) == int ? self.current <= self.min : double(self.current) / double(self.available) >= double(self.min.replace('%', '')) / 100.0": 629162,
|
||||
},
|
||||
},
|
||||
{name: "preserve unknown fields",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"withUnknown": {
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
},
|
||||
"withUnknownList": listType(&schema.Structural{
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
}),
|
||||
"withUnknownFieldList": listType(&schema.Structural{
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Properties: map[string]schema.Structural{
|
||||
"fieldOfUnknownType": {
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
"anyvalList": listType(&schema.Structural{
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
}),
|
||||
"anyvalMap": mapType(&schema.Structural{
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
}),
|
||||
"anyvalField1": {
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
},
|
||||
"anyvalField2": {
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"has(self.withUnknown)": 2,
|
||||
"self.withUnknownList.size() == 5": 4,
|
||||
// fields that are unknown because they were not specified on the object schema are included in equality checks
|
||||
"self.withUnknownList[0] != self.withUnknownList[1]": 6,
|
||||
"self.withUnknownList[1] == self.withUnknownList[2]": 6,
|
||||
"self.withUnknownList[3] == self.withUnknownList[4]": 6,
|
||||
|
||||
// fields specified on the object schema that are unknown because the field's schema is unknown are also included equality checks
|
||||
"self.withUnknownFieldList[0] != self.withUnknownFieldList[1]": 6,
|
||||
"self.withUnknownFieldList[1] == self.withUnknownFieldList[2]": 6,
|
||||
},
|
||||
},
|
||||
{name: "known and unknown fields",
|
||||
schema: &schema.Structural{
|
||||
Generic: schema.Generic{
|
||||
Type: "object",
|
||||
},
|
||||
Properties: map[string]schema.Structural{
|
||||
"withUnknown": {
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
Properties: map[string]schema.Structural{
|
||||
"known": integerType,
|
||||
},
|
||||
},
|
||||
"withUnknownList": listType(&schema.Structural{
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Extensions: schema.Extensions{
|
||||
XPreserveUnknownFields: true,
|
||||
},
|
||||
Properties: map[string]schema.Structural{
|
||||
"known": integerType,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectCost: map[string]uint64{
|
||||
"self.withUnknown.known == 1": 3,
|
||||
// if the unknown fields are the same, they are equal
|
||||
"self.withUnknownList[1] == self.withUnknownList[2]": 6,
|
||||
|
||||
// if unknown fields are different, they are not equal
|
||||
"self.withUnknownList[0] != self.withUnknownList[1]": 6,
|
||||
"self.withUnknownList[0] != self.withUnknownList[3]": 6,
|
||||
"self.withUnknownList[0] != self.withUnknownList[5]": 6,
|
||||
|
||||
// if all fields are known, equality works as usual
|
||||
"self.withUnknownList[3] == self.withUnknownList[4]": 6,
|
||||
"self.withUnknownList[4] != self.withUnknownList[5]": 6,
|
||||
},
|
||||
},
|
||||
{name: "field nullability",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"unsetPlainStr": stringType,
|
||||
"unsetDefaultedStr": withDefault("default value", stringType),
|
||||
"unsetNullableStr": withNullable(true, stringType),
|
||||
|
||||
"setPlainStr": stringType,
|
||||
"setDefaultedStr": withDefault("default value", stringType),
|
||||
"setNullableStr": withNullable(true, stringType),
|
||||
"setToNullNullableStr": withNullable(true, stringType),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"!has(self.unsetPlainStr)": 3,
|
||||
"has(self.unsetDefaultedStr) && self.unsetDefaultedStr == 'default value'": 6,
|
||||
"!has(self.unsetNullableStr)": 3,
|
||||
|
||||
"has(self.setPlainStr) && self.setPlainStr == 'v1'": 5,
|
||||
"has(self.setDefaultedStr) && self.setDefaultedStr == 'v2'": 5,
|
||||
"has(self.setNullableStr) && self.setNullableStr == 'v3'": 5,
|
||||
// We treat null fields as absent fields, not as null valued fields.
|
||||
// Note that this is different than how we treat nullable list items or map values.
|
||||
"type(self.setNullableStr) != null_type": 4,
|
||||
|
||||
// a field that is set to null is treated the same as an absent field in validation rules
|
||||
"!has(self.setToNullNullableStr)": 3,
|
||||
},
|
||||
},
|
||||
{name: "null values in container types",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"m": mapType(withNullablePtr(true, stringType)),
|
||||
"l": listType(withNullablePtr(true, stringType)),
|
||||
"s": listSetType(withNullablePtr(true, stringType)),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.m.size() == 2": 4,
|
||||
"'a' in self.m": 3,
|
||||
"type(self.m['a']) == null_type": 5, // null check using runtime type checking
|
||||
},
|
||||
},
|
||||
{name: "object types are not accessible",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"nestedInMap": mapType(objectTypePtr(map[string]schema.Structural{
|
||||
"inMapField": integerType,
|
||||
})),
|
||||
"nestedInList": listType(objectTypePtr(map[string]schema.Structural{
|
||||
"inListField": integerType,
|
||||
})),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
// we do not expose a stable type for the self variable, even when it is an object that CEL
|
||||
// considers a named type. The only operation developers should be able to perform on the type is
|
||||
// equality checking.
|
||||
"type(self) == type(self)": uint64(1844674407370955268),
|
||||
"type(self.nestedInMap['k1']) == type(self.nestedInMap['k2'])": uint64(1844674407370955272),
|
||||
},
|
||||
},
|
||||
{name: "listMaps with unsupported identity characters in property names",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"objs": listType(listMapTypePtr([]string{"k!", "k."}, objectTypePtr(map[string]schema.Structural{
|
||||
"k!": stringType,
|
||||
"k.": stringType,
|
||||
}))),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.objs[0] == self.objs[1]": 104864, // equal even though order is different
|
||||
"self.objs[0][0].k__dot__ == '1'": 6, // '.' is a supported character in identifiers, but it is escaped
|
||||
},
|
||||
},
|
||||
{name: "container type composition",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"obj": objectType(map[string]schema.Structural{
|
||||
"field": stringType,
|
||||
}),
|
||||
"mapOfMap": mapType(mapTypePtr(&stringType)),
|
||||
"mapOfObj": mapType(objectTypePtr(map[string]schema.Structural{
|
||||
"field2": stringType,
|
||||
})),
|
||||
"mapOfListMap": mapType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
|
||||
"k": stringType,
|
||||
"v": stringType,
|
||||
}))),
|
||||
"mapOfList": mapType(listTypePtr(&stringType)),
|
||||
"listMapOfObj": listMapType([]string{"k2"}, objectTypePtr(map[string]schema.Structural{
|
||||
"k2": stringType,
|
||||
"v2": stringType,
|
||||
})),
|
||||
"listOfMap": listType(mapTypePtr(&stringType)),
|
||||
"listOfObj": listType(objectTypePtr(map[string]schema.Structural{
|
||||
"field3": stringType,
|
||||
})),
|
||||
"listOfListMap": listType(listMapTypePtr([]string{"k3"}, objectTypePtr(map[string]schema.Structural{
|
||||
"k3": stringType,
|
||||
"v3": stringType,
|
||||
}))),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"self.obj.field == 'a'": 4,
|
||||
"self.mapOfMap['x']['y'] == 'b'": 5,
|
||||
"self.mapOfObj['k'].field2 == 'c'": 5,
|
||||
"self.mapOfListMap['o'].exists(e, e.k == '1' && e.v == 'd')": 10485754,
|
||||
"self.mapOfList['l'][0] == 'e'": 5,
|
||||
"self.listMapOfObj.exists(e, e.k2 == '2' && e.v2 == 'f')": 10485753,
|
||||
"self.listOfMap[0]['z'] == 'g'": 5,
|
||||
"self.listOfObj[0].field3 == 'h'": 5,
|
||||
"self.listOfListMap[0].exists(e, e.k3 == '3' && e.v3 == 'i')": 10485754,
|
||||
},
|
||||
},
|
||||
{name: "optionals",
|
||||
schema: objectTypePtr(map[string]schema.Structural{
|
||||
"obj": objectType(map[string]schema.Structural{
|
||||
"field": stringType,
|
||||
"absentField": stringType,
|
||||
}),
|
||||
"m": mapType(&stringType),
|
||||
"l": listType(&stringType),
|
||||
}),
|
||||
expectCost: map[string]uint64{
|
||||
"optional.of('a') != optional.of('b')": uint64(1844674407370955266),
|
||||
"optional.of('a') != optional.none()": uint64(1844674407370955266),
|
||||
"optional.of('a').hasValue()": 2,
|
||||
"optional.of('a').or(optional.of('a')).hasValue()": 4, // or() is short-circuited
|
||||
"optional.none().or(optional.of('a')).hasValue()": 4,
|
||||
"optional.of('a').optMap(v, v == 'value').hasValue()": 17,
|
||||
"self.obj.?field == optional.of('a')": uint64(1844674407370955268),
|
||||
"self.obj.?absentField == optional.none()": uint64(1844674407370955268),
|
||||
"self.obj.?field.orValue('v') == 'a'": 5,
|
||||
"self.m[?'k'] == optional.of('v')": uint64(1844674407370955268),
|
||||
"self.l[?0] == optional.of('a')": uint64(1844674407370955268),
|
||||
"optional.ofNonZeroValue(1).hasValue()": 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for validRule, expectedCost := range tt.expectCost {
|
||||
validRule := validRule
|
||||
expectedCost := expectedCost
|
||||
testName := validRule
|
||||
if len(testName) > 127 {
|
||||
testName = testName[:127]
|
||||
}
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := withRule(*tt.schema, validRule)
|
||||
t.Run("calc maxLength", schemaChecker(&s, uint64(expectedCost), 0, t))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -409,6 +409,124 @@ func TestAuthzLibrary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuantityCost(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
expectEstimatedCost checker.CostEstimate
|
||||
expectRuntimeCost uint64
|
||||
}{
|
||||
{
|
||||
name: "path",
|
||||
expr: `quantity("12Mi")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
||||
expectRuntimeCost: 1,
|
||||
},
|
||||
{
|
||||
name: "isQuantity",
|
||||
expr: `isQuantity("20")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
||||
expectRuntimeCost: 1,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_megabytes",
|
||||
expr: `isQuantity("20M")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
|
||||
expectRuntimeCost: 1,
|
||||
},
|
||||
{
|
||||
name: "equality_reflexivity",
|
||||
expr: `quantity("200M") == quantity("200M")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 1844674407370955266},
|
||||
expectRuntimeCost: 3,
|
||||
},
|
||||
{
|
||||
name: "equality_symmetry",
|
||||
expr: `quantity("200M") == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3689348814741910532},
|
||||
expectRuntimeCost: 6,
|
||||
},
|
||||
{
|
||||
name: "equality_transitivity",
|
||||
expr: `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 5534023222112865798},
|
||||
expectRuntimeCost: 9,
|
||||
},
|
||||
{
|
||||
name: "quantity_less",
|
||||
expr: `quantity("50M").isLessThan(quantity("50Mi"))`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
||||
expectRuntimeCost: 3,
|
||||
},
|
||||
{
|
||||
name: "quantity_greater",
|
||||
expr: `quantity("50Mi").isGreaterThan(quantity("50M"))`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
||||
expectRuntimeCost: 3,
|
||||
},
|
||||
{
|
||||
name: "compare_equal",
|
||||
expr: `quantity("200M").compareTo(quantity("0.2G")) > 0`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
|
||||
expectRuntimeCost: 4,
|
||||
},
|
||||
{
|
||||
name: "add_quantity",
|
||||
expr: `quantity("50k").add(quantity("20")) == quantity("50.02k")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 1844674407370955268},
|
||||
expectRuntimeCost: 5,
|
||||
},
|
||||
{
|
||||
name: "sub_quantity",
|
||||
expr: `quantity("50k").sub(quantity("20")) == quantity("49.98k")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 5, Max: 1844674407370955268},
|
||||
expectRuntimeCost: 5,
|
||||
},
|
||||
{
|
||||
name: "sub_int",
|
||||
expr: `quantity("50k").sub(20) == quantity("49980")`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 1844674407370955267},
|
||||
expectRuntimeCost: 4,
|
||||
},
|
||||
{
|
||||
name: "arith_chain_1",
|
||||
expr: `quantity("50k").add(20).sub(quantity("100k")).asInteger() > 0`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
||||
expectRuntimeCost: 6,
|
||||
},
|
||||
{
|
||||
name: "arith_chain",
|
||||
expr: `quantity("50k").add(20).sub(quantity("100k")).sub(-50000).asInteger() > 0`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 7, Max: 7},
|
||||
expectRuntimeCost: 7,
|
||||
},
|
||||
{
|
||||
name: "as_integer",
|
||||
expr: `quantity("50k").asInteger() > 0`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
||||
expectRuntimeCost: 3,
|
||||
},
|
||||
{
|
||||
name: "is_integer",
|
||||
expr: `quantity("50").isInteger()`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
||||
expectRuntimeCost: 2,
|
||||
},
|
||||
{
|
||||
name: "as_float",
|
||||
expr: `quantity("50.703k").asApproximateFloat() > 0.0`,
|
||||
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
|
||||
expectRuntimeCost: 3,
|
||||
},
|
||||
}
|
||||
|
||||
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(
|
||||
@ -417,6 +535,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
Quantity(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
|
Loading…
Reference in New Issue
Block a user