Merge pull request #121455 from cici37/test_cost

CRD validation rule: Add stability tests for CEL cost estimation
This commit is contained in:
Kubernetes Prow Robot 2023-10-24 21:00:58 +02:00 committed by GitHub
commit 03ba7efb85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 913 additions and 0 deletions

View File

@ -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))
})
}
})
}
}

View File

@ -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)