1
16
17 package cel
18
19 import (
20 "bytes"
21 "context"
22 "flag"
23 "fmt"
24 "math"
25 "strings"
26 "sync"
27 "testing"
28 "time"
29
30 "github.com/stretchr/testify/assert"
31 "github.com/stretchr/testify/require"
32
33 "k8s.io/klog/v2"
34 "k8s.io/kube-openapi/pkg/validation/strfmt"
35 "k8s.io/utils/ptr"
36
37 apiextensionsinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
38 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
39 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
40 "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
41 apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
42 "k8s.io/apimachinery/pkg/util/validation/field"
43 "k8s.io/apimachinery/pkg/util/yaml"
44 celconfig "k8s.io/apiserver/pkg/apis/cel"
45 "k8s.io/apiserver/pkg/cel/common"
46 utilfeature "k8s.io/apiserver/pkg/util/feature"
47 "k8s.io/apiserver/pkg/warning"
48 featuregatetesting "k8s.io/component-base/featuregate/testing"
49 )
50
51
52 func TestValidationExpressions(t *testing.T) {
53 tests := []struct {
54 name string
55 schema *schema.Structural
56 oldSchema *schema.Structural
57 obj interface{}
58 oldObj interface{}
59 valid []string
60 errors map[string]string
61 costBudget int64
62 isRoot bool
63 expectSkipped bool
64 }{
65
66
67 {name: "integers",
68
69 obj: objs(math.MaxInt64, math.MaxInt64, math.MaxInt32, math.MaxInt32, math.MaxInt64, math.MaxInt64),
70 schema: schemas(integerType, integerType, int32Type, int32Type, int64Type, int64Type),
71 valid: []string{
72 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%d", math.MaxInt64)),
73 ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", fmt.Sprintf("%d", math.MaxInt32)),
74 ValsEqualThemselvesAndDataLiteral("self.val5", "self.val6", fmt.Sprintf("%d", math.MaxInt64)),
75 "self.val1 == self.val6",
76 "type(self.val1) == int",
77 fmt.Sprintf("self.val3 + 1 == %d + 1", math.MaxInt32),
78 "type(self.val3) == int",
79 "type(self.val5) == int",
80 },
81 errors: map[string]string{
82 "self.val1 + 1 == 0": "integer overflow",
83 "self.val5 + 1 == 0": "integer overflow",
84 "1 / 0 == 1 / 0": "division by zero",
85 },
86 },
87 {name: "numbers",
88 obj: objs(math.MaxFloat64, math.MaxFloat64, math.MaxFloat32, math.MaxFloat32, math.MaxFloat64, math.MaxFloat64, int64(1)),
89 schema: schemas(numberType, numberType, floatType, floatType, doubleType, doubleType, doubleType),
90 valid: []string{
91 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", fmt.Sprintf("%f", math.MaxFloat64)),
92 ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", fmt.Sprintf("%f", math.MaxFloat32)),
93 ValsEqualThemselvesAndDataLiteral("self.val5", "self.val6", fmt.Sprintf("%f", math.MaxFloat64)),
94 "self.val1 == self.val6",
95 "type(self.val1) == double",
96 "type(self.val3) == double",
97 "type(self.val5) == double",
98
99
100
101
102 "type(self.val7) == double",
103 "self.val7 == 1.0",
104 },
105 },
106 {name: "numeric comparisons",
107 obj: objs(
108 int64(5),
109 int64(10),
110 int64(15),
111 float64(10.0),
112 float64(10.0),
113 float64(10.0),
114 int64(10),
115 int64(10),
116 int64(10),
117 ),
118 schema: schemas(integerType, integerType, integerType, numberType, floatType, doubleType, numberType, floatType, doubleType),
119 valid: []string{
120
121
122
123 "double(self.val1) < self.val4",
124 "double(self.val1) <= self.val4",
125 "double(self.val2) <= self.val4",
126 "double(self.val2) == self.val4",
127 "double(self.val2) >= self.val4",
128 "double(self.val3) > self.val4",
129 "double(self.val3) >= self.val4",
130
131 "self.val1 < int(self.val4)",
132 "self.val2 == int(self.val4)",
133 "self.val3 > int(self.val4)",
134
135 "double(self.val1) < self.val5",
136 "double(self.val2) == self.val5",
137 "double(self.val3) > self.val5",
138
139 "self.val1 < int(self.val5)",
140 "self.val2 == int(self.val5)",
141 "self.val3 > int(self.val5)",
142
143 "double(self.val1) < self.val6",
144 "double(self.val2) == self.val6",
145 "double(self.val3) > self.val6",
146
147 "self.val1 < int(self.val6)",
148 "self.val2 == int(self.val6)",
149 "self.val3 > int(self.val6)",
150
151
152
153 "double(self.val1) < self.val7",
154 "double(self.val2) == self.val7",
155 "double(self.val3) > self.val7",
156
157 "self.val1 < int(self.val7)",
158 "self.val2 == int(self.val7)",
159 "self.val3 > int(self.val7)",
160
161 "double(self.val1) < self.val8",
162 "double(self.val2) == self.val8",
163 "double(self.val3) > self.val8",
164
165 "self.val1 < int(self.val8)",
166 "self.val2 == int(self.val8)",
167 "self.val3 > int(self.val8)",
168
169 "double(self.val1) < self.val9",
170 "double(self.val2) == self.val9",
171 "double(self.val3) > self.val9",
172
173 "self.val1 < int(self.val9)",
174 "self.val2 == int(self.val9)",
175 "self.val3 > int(self.val9)",
176
177
178 "double(5) < 10.0",
179 "double(10) == 10.0",
180 "double(15) > 10.0",
181
182 "5 < int(10.0)",
183 "10 == int(10.0)",
184 "15 > int(10.0)",
185
186
187 "double(self.val1) < 10.0",
188 "double(self.val2) == 10.0",
189 "double(self.val3) > 10.0",
190
191
192 "self.val1 < self.val4",
193 "self.val1 <= self.val4",
194 "self.val2 <= self.val4",
195 "self.val2 >= self.val4",
196 "self.val3 > self.val4",
197 "self.val3 >= self.val4",
198
199 "self.val1 < self.val4",
200 "self.val3 > self.val4",
201
202 "self.val1 < self.val5",
203 "self.val3 > self.val5",
204
205 "self.val1 < self.val5",
206 "self.val3 > self.val5",
207
208 "self.val1 < self.val6",
209 "self.val3 > self.val6",
210
211 "self.val1 < self.val6",
212 "self.val3 > self.val6",
213
214
215
216 "self.val1 < self.val7",
217 "self.val3 > self.val7",
218
219 "self.val1 < int(self.val7)",
220 "self.val3 > int(self.val7)",
221
222 "self.val1 < self.val8",
223 "self.val3 > self.val8",
224
225 "self.val1 < self.val8",
226 "self.val3 > self.val8",
227
228 "self.val1 < self.val9",
229 "self.val3 > self.val9",
230
231 "self.val1 < self.val9",
232 "self.val3 > self.val9",
233
234
235 "5 < 10.0",
236 "15 > 10.0",
237
238 "5 < 10.0",
239 "15 > 10.0",
240
241
242 "self.val1 < 10.0",
243 "self.val3 > 10.0",
244 },
245 },
246 {name: "unicode strings",
247 obj: objs("Rook takes π", "Rook takes π"),
248 schema: schemas(stringType, stringType),
249 valid: []string{
250 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'Rook takes π'"),
251 "self.val1.startsWith('Rook')",
252 "!self.val1.startsWith('knight')",
253 "self.val1.contains('takes')",
254 "!self.val1.contains('gives')",
255 "self.val1.endsWith('π')",
256 "!self.val1.endsWith('pawn')",
257 "self.val1.matches('^[^0-9]*$')",
258 "!self.val1.matches('^[0-9]*$')",
259 "type(self.val1) == string",
260 "size(self.val1) == 12",
261
262
263 "self.val1.charAt(3) == 'k'",
264 "self.val1.indexOf('o') == 1",
265 "self.val1.indexOf('o', 2) == 2",
266 "self.val1.replace(' ', 'x') == 'Rookxtakesxπ'",
267 "self.val1.replace(' ', 'x', 1) == 'Rookxtakes π'",
268 "self.val1.split(' ') == ['Rook', 'takes', 'π']",
269 "self.val1.split(' ', 2) == ['Rook', 'takes π']",
270 "self.val1.substring(5) == 'takes π'",
271 "self.val1.substring(0, 4) == 'Rook'",
272 "self.val1.substring(4, 10).trim() == 'takes'",
273 "self.val1.upperAscii() == 'ROOK TAKES π'",
274 "self.val1.lowerAscii() == 'rook takes π'",
275
276 "'%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'",
277 "'%e'.format([3.14]) == '3.140000β―Γβ―10β°β°'",
278 "'%o %o %o'.format([7, 8, 9]) == '7 10 11'",
279 "'%b %b %b'.format([7, 8, 9]) == '111 1000 1001'",
280 },
281 errors: map[string]string{
282
283 "self.val1.matches(')')": "compile error: compilation failed: ERROR: <input>:1:19: invalid matches argument",
284 },
285 },
286 {name: "escaped strings",
287 obj: objs("l1\nl2", "l1\nl2"),
288 schema: schemas(stringType, stringType),
289 valid: []string{
290 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "'l1\\nl2'"),
291 "self.val1 == '''l1\nl2'''",
292 },
293 },
294 {name: "bytes",
295 obj: objs("QUI=", "QUI="),
296 schema: schemas(byteType, byteType),
297 valid: []string{
298 "self.val1 == self.val2",
299 "self.val1 == b'AB'",
300 "self.val1 == bytes('AB')",
301 "self.val1 == b'\\x41\\x42'",
302 "type(self.val1) == bytes",
303 "size(self.val1) == 2",
304 },
305 },
306 {name: "booleans",
307 obj: objs(true, true, false, false),
308 schema: schemas(booleanType, booleanType, booleanType, booleanType),
309 valid: []string{
310 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "true"),
311 ValsEqualThemselvesAndDataLiteral("self.val3", "self.val4", "false"),
312 "self.val1 != self.val4",
313 "type(self.val1) == bool",
314 },
315 },
316 {name: "duration format",
317 obj: objs("1h2m3s4ms", "1h2m3s4ms"),
318 schema: schemas(durationFormat, durationFormat),
319 valid: []string{
320 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "duration('1h2m3s4ms')"),
321 "self.val1 == duration('1h2m') + duration('3s4ms')",
322 "self.val1.getHours() == 1",
323 "self.val1.getMinutes() == 62",
324 "self.val1.getSeconds() == 3723",
325 "self.val1.getMilliseconds() == 3723004",
326 "type(self.val1) == google.protobuf.Duration",
327 },
328 errors: map[string]string{
329 "duration('1')": "compilation failed: ERROR: <input>:1:10: invalid duration argument",
330 "duration('1d')": "compilation failed: ERROR: <input>:1:10: invalid duration argument",
331 "duration('1us') < duration('1nns')": "compilation failed: ERROR: <input>:1:28: invalid duration argument",
332 },
333 },
334 {name: "date format",
335 obj: objs("1997-07-16", "1997-07-16"),
336 schema: schemas(dateFormat, dateFormat),
337 valid: []string{
338 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('1997-07-16T00:00:00.000Z')"),
339 "self.val1.getDate() == 16",
340 "self.val1.getMonth() == 06",
341 "self.val1.getFullYear() == 1997",
342 "type(self.val1) == google.protobuf.Timestamp",
343 },
344 },
345 {name: "date-time format",
346 obj: objs("2011-08-18T19:03:37.010000000+01:00", "2011-08-18T19:03:37.010000000+01:00"),
347 schema: schemas(dateTimeFormat, dateTimeFormat),
348 valid: []string{
349 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "timestamp('2011-08-18T19:03:37.010+01:00')"),
350 "self.val1 == timestamp('2011-08-18T00:00:00.000+01:00') + duration('19h3m37s10ms')",
351 "self.val1.getDate('01:00') == 18",
352 "self.val1.getMonth('01:00') == 7",
353 "self.val1.getFullYear('01:00') == 2011",
354 "self.val1.getHours('01:00') == 19",
355 "self.val1.getMinutes('01:00') == 03",
356 "self.val1.getSeconds('01:00') == 37",
357 "self.val1.getMilliseconds('01:00') == 10",
358 "self.val1.getHours('UTC') == 18",
359 "type(self.val1) == google.protobuf.Timestamp",
360 },
361 errors: map[string]string{
362 "timestamp('1000-00-00T00:00:00Z')": "compilation failed: ERROR: <input>:1:11: invalid timestamp",
363 "timestamp('1000-01-01T00:00:00ZZ')": "compilation failed: ERROR: <input>:1:11: invalid timestamp",
364 "timestamp(-62135596801)": "compilation failed: ERROR: <input>:1:11: invalid timestamp",
365 },
366 },
367 {name: "enums",
368 obj: map[string]interface{}{"enumStr": "Pending"},
369 schema: objectTypePtr(map[string]schema.Structural{"enumStr": {
370 Generic: schema.Generic{
371 Type: "string",
372 },
373 ValueValidation: &schema.ValueValidation{
374 Enum: []schema.JSON{
375 {Object: "Pending"},
376 {Object: "Available"},
377 {Object: "Bound"},
378 {Object: "Released"},
379 {Object: "Failed"},
380 },
381 },
382 }}),
383 valid: []string{
384 "self.enumStr == 'Pending'",
385 "self.enumStr in ['Pending', 'Available']",
386 },
387 },
388 {name: "conversions",
389 obj: objs(int64(10), 10.0, 10.49, 10.5, true, "10", "MTA=", "3723.004s", "1h2m3s4ms", "2011-08-18T19:03:37.01+01:00", "2011-08-18T19:03:37.01+01:00", "2011-08-18T00:00:00Z", "2011-08-18"),
390 schema: schemas(integerType, numberType, numberType, numberType, booleanType, stringType, byteType, stringType, durationFormat, stringType, dateTimeFormat, stringType, dateFormat),
391 valid: []string{
392 "int(self.val2) == self.val1",
393 "int(self.val3) == self.val1",
394 "int(self.val4) == self.val1",
395 "int(self.val6) == self.val1",
396 "double(self.val1) == self.val2",
397 "double(self.val6) == self.val2",
398 "bytes(self.val6) == self.val7",
399 "string(self.val1) == self.val6",
400 "string(self.val2) == '10'",
401 "string(self.val3) == '10.49'",
402 "string(self.val4) == '10.5'",
403 "string(self.val5) == 'true'",
404 "string(self.val7) == self.val6",
405 "duration(self.val8) == self.val9",
406 "string(self.val9) == self.val8",
407 "timestamp(self.val10) == self.val11",
408 "string(self.val11) == self.val10",
409 "timestamp(self.val12) == self.val13",
410 "string(self.val13) == self.val12",
411 },
412 },
413 {name: "lists",
414 obj: objs([]interface{}{1, 2, 3}, []interface{}{1, 2, 3}),
415 schema: schemas(listType(&integerType), listType(&integerType)),
416 valid: []string{
417 ValsEqualThemselvesAndDataLiteral("self.val1", "self.val2", "[1, 2, 3]"),
418 "1 in self.val1",
419 "self.val2[0] in self.val1",
420 "!(0 in self.val1)",
421 "self.val1 + self.val2 == [1, 2, 3, 1, 2, 3]",
422 "self.val1 + [4, 5] == [1, 2, 3, 4, 5]",
423 },
424 errors: map[string]string{
425
426 "[1, 'a', false].filter(x, string(x) == 'a')": "compilation failed: ERROR: <input>:1:5: expected type 'int' but found 'string'",
427 },
428 },
429 {name: "string lists",
430 obj: objs([]interface{}{"a", "b", "c"}),
431 schema: schemas(listType(&stringType)),
432 valid: []string{
433
434 "self.val1.join('-') == 'a-b-c'",
435 "['a', 'b', 'c'].join('-') == 'a-b-c'",
436 "self.val1.join() == 'abc'",
437 "['a', 'b', 'c'].join() == 'abc'",
438 },
439 },
440 {name: "listSets",
441 obj: objs([]interface{}{"a", "b", "c"}, []interface{}{"a", "c", "b"}),
442 schema: schemas(listSetType(&stringType), listSetType(&stringType)),
443 valid: []string{
444
445 "self.val1 == ['c', 'b', 'a']",
446 "self.val1 == self.val2",
447 "'a' in self.val1",
448 "self.val2[0] in self.val1",
449 "!('x' in self.val1)",
450 "self.val1 + self.val2 == ['a', 'b', 'c']",
451 "self.val1 + ['c', 'd'] == ['a', 'b', 'c', 'd']",
452
453
454 "self.val1.join('-') == 'a-b-c'",
455 "['a', 'b', 'c'].join('-') == 'a-b-c'",
456 "self.val1.join() == 'abc'",
457 "['a', 'b', 'c'].join() == 'abc'",
458
459
460 "sets.contains(['a', 'b'], [])",
461 "sets.contains(['a', 'b'], ['b'])",
462 "!sets.contains(['a', 'b'], ['c'])",
463 "sets.equivalent([], [])",
464 "sets.equivalent(['c', 'b', 'a'], ['b', 'c', 'a'])",
465 "!sets.equivalent(['a', 'b'], ['b', 'c'])",
466 "sets.intersects(['a', 'b'], ['b', 'c'])",
467 "!sets.intersects([], [])",
468 "!sets.intersects(['a', 'b'], [])",
469 "!sets.intersects(['a', 'b'], ['c', 'd'])",
470
471 "sets.contains([1, 2], [2])",
472 "sets.contains([true, false], [false])",
473 "sets.contains([1.25, 1.5], [1.5])",
474 "sets.contains([{'a': 1}, {'b': 2}], [{'b': 2}])",
475 "sets.contains([[1, 2], [3, 4]], [[3, 4]])",
476 "sets.contains([timestamp('2000-01-01T00:00:00.000+01:00'), timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
477 "sets.contains([duration('1h'), duration('2h')], [duration('2h')])",
478
479 "sets.equivalent([1, 2], [1, 2])",
480 "sets.equivalent([true, false], [true, false])",
481 "sets.equivalent([1.25, 1.5], [1.25, 1.5])",
482 "sets.equivalent([{'a': 1}, {'b': 2}], [{'a': 1}, {'b': 2}])",
483 "sets.equivalent([[1, 2], [3, 4]], [[1, 2], [3, 4]])",
484 "sets.equivalent([timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
485 "sets.equivalent([duration('1h'), duration('2h')], [duration('1h'), duration('2h')])",
486
487 "sets.intersects([1, 2], [2])",
488 "sets.intersects([true, false], [false])",
489 "sets.intersects([1.25, 1.5], [1.5])",
490 "sets.intersects([{'a': 1}, {'b': 2}], [{'b': 2}])",
491 "sets.intersects([[1, 2], [3, 4]], [[3, 4]])",
492 "sets.intersects([timestamp('2000-01-01T00:00:00.000+01:00'), timestamp('2012-01-01T00:00:00.000+01:00')], [timestamp('2012-01-01T00:00:00.000+01:00')])",
493 "sets.intersects([duration('1h'), duration('2h')], [duration('2h')])",
494 },
495 },
496 {name: "listMaps",
497 obj: map[string]interface{}{
498 "objs": []interface{}{
499 []interface{}{
500 map[string]interface{}{"k": "a", "v": "1"},
501 map[string]interface{}{"k": "b", "v": "2"},
502 },
503 []interface{}{
504 map[string]interface{}{"k": "b", "v": "2"},
505 map[string]interface{}{"k": "a", "v": "1"},
506 },
507 []interface{}{
508 map[string]interface{}{"k": "b", "v": "3"},
509 map[string]interface{}{"k": "a", "v": "1"},
510 },
511 []interface{}{
512 map[string]interface{}{"k": "c", "v": "4"},
513 },
514 },
515 },
516 schema: objectTypePtr(map[string]schema.Structural{
517 "objs": listType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
518 "k": stringType,
519 "v": stringType,
520 }))),
521 }),
522 valid: []string{
523 "self.objs[0] == self.objs[1]",
524 "self.objs[0] + self.objs[2] == self.objs[2]",
525 "self.objs[2] + self.objs[0] == self.objs[0]",
526
527 "self.objs[0] == [self.objs[0][0], self.objs[0][1]]",
528 "self.objs[0] == [self.objs[0][1], self.objs[0][0]]",
529
530 "self.objs[2] + [self.objs[0][0], self.objs[0][1]] == self.objs[0]",
531 "size(self.objs[0] + [self.objs[3][0]]) == 3",
532 },
533 errors: map[string]string{
534 "self.objs[0] == {'k': 'a', 'v': '1'}": "no matching overload for '_==_'",
535 },
536 },
537 {name: "maps",
538 obj: objs(map[string]interface{}{"k1": "a", "k2": "b"}, map[string]interface{}{"k2": "b", "k1": "a"}),
539 schema: schemas(mapType(&stringType), mapType(&stringType)),
540 valid: []string{
541 "self.val1 == self.val2",
542 "'k1' in self.val1",
543 "!('k3' in self.val1)",
544 "self.val1 == {'k1': 'a', 'k2': 'b'}",
545 },
546 errors: map[string]string{
547
548 "{'k1': 'a', 'k2': 1, 'k2': false}": "expected type 'string' but found 'int'",
549 },
550 },
551 {name: "objects",
552 obj: map[string]interface{}{
553 "objs": []interface{}{
554 map[string]interface{}{"f1": "a", "f2": "b"},
555 map[string]interface{}{"f1": "a", "f2": "b"},
556 },
557 },
558 schema: objectTypePtr(map[string]schema.Structural{
559 "objs": listType(objectTypePtr(map[string]schema.Structural{
560 "f1": stringType,
561 "f2": stringType,
562 })),
563 }),
564 valid: []string{
565 "self.objs[0] == self.objs[1]",
566 },
567 errors: map[string]string{
568 "self.objs[0] == {'f1': 'a', 'f2': 'b'}": "found no matching overload for '_==_'",
569 },
570 },
571 {name: "object access",
572 obj: map[string]interface{}{
573 "a": map[string]interface{}{
574 "b": 1,
575 "d": nil,
576 },
577 "a1": map[string]interface{}{
578 "b1": map[string]interface{}{
579 "c1": 4,
580 },
581 },
582 "a3": map[string]interface{}{},
583 },
584 schema: objectTypePtr(map[string]schema.Structural{
585 "a": objectType(map[string]schema.Structural{
586 "b": integerType,
587 "c": integerType,
588 "d": withNullable(true, integerType),
589 }),
590 "a1": objectType(map[string]schema.Structural{
591 "b1": objectType(map[string]schema.Structural{
592 "c1": integerType,
593 }),
594 "d2": objectType(map[string]schema.Structural{
595 "e2": integerType,
596 }),
597 }),
598 }),
599
600 valid: []string{
601 "has(self.a.b)",
602 "!has(self.a.c)",
603 "!has(self.a.d)",
604 "has(self.a1.b1.c1)",
605 "!(has(self.a1.d2) && has(self.a1.d2.e2))",
606 "!has(self.a1.d2)",
607 },
608 errors: map[string]string{
609 "has(self.a.z)": "undefined field 'z'",
610 "self.a['b'] == 1": "no matching overload for '_[_]'",
611 "has(self.a1.d2.e2)": "no such key: d2",
612 },
613 },
614 {name: "map access",
615 obj: map[string]interface{}{
616 "val": map[string]interface{}{
617 "b": 1,
618 "d": 2,
619 },
620 },
621 schema: objectTypePtr(map[string]schema.Structural{
622 "val": mapType(&integerType),
623 }),
624 valid: []string{
625
626 "!('a' in self.val)",
627 "'b' in self.val",
628 "!('c' in self.val)",
629 "'d' in self.val",
630
631 "!has(self.val.a)",
632 "has(self.val.b)",
633 "!has(self.val.c)",
634 "has(self.val.d)",
635 "self.val.all(k, self.val[k] > 0)",
636 "self.val.exists(k, self.val[k] > 1)",
637 "self.val.exists_one(k, self.val[k] == 2)",
638 "!self.val.exists(k, self.val[k] > 2)",
639 "!self.val.exists_one(k, self.val[k] > 0)",
640 "size(self.val) == 2",
641 "self.val.map(k, self.val[k]).exists(v, v == 1)",
642 "size(self.val.filter(k, self.val[k] > 1)) == 1",
643 },
644 errors: map[string]string{
645 "self.val['c'] == 1": "no such key: c",
646 },
647 },
648 {name: "listMap access",
649 obj: map[string]interface{}{
650 "listMap": []interface{}{
651 map[string]interface{}{"k": "a1", "v": "b1"},
652 map[string]interface{}{"k": "a2", "v": "b2"},
653 map[string]interface{}{"k": "a3", "v": "b3", "v2": "z"},
654 },
655 },
656 schema: objectTypePtr(map[string]schema.Structural{
657 "listMap": listMapType([]string{"k"}, objectTypePtr(map[string]schema.Structural{
658 "k": stringType,
659 "v": stringType,
660 "v2": stringType,
661 })),
662 }),
663 valid: []string{
664 "has(self.listMap[0].v)",
665 "self.listMap.all(m, m.k.startsWith('a'))",
666 "self.listMap.all(m, !has(m.v2) || m.v2 == 'z')",
667 "self.listMap.exists(m, m.k.endsWith('1'))",
668 "self.listMap.exists_one(m, m.k == 'a3')",
669 "!self.listMap.all(m, m.k.endsWith('1'))",
670 "!self.listMap.exists(m, m.v == 'x')",
671 "!self.listMap.exists_one(m, m.k.startsWith('a'))",
672 "size(self.listMap.filter(m, m.k == 'a1')) == 1",
673 "self.listMap.exists(m, m.k == 'a1' && m.v == 'b1')",
674 "self.listMap.map(m, m.v).exists(v, v == 'b1')",
675
676
677
678
679 "self.listMap.exists(m, has(m.v2) && m.v2 == 'z')",
680 "!self.listMap.all(m, has(m.v2) && m.v2 != 'z')",
681 "self.listMap.exists_one(m, has(m.v2) && m.v2 == 'z')",
682 "self.listMap.filter(m, has(m.v2) && m.v2 == 'z').size() == 1",
683
684 "self.listMap.map(m, has(m.v2) && m.v2 == 'z', m.v2).size() == 1",
685 "self.listMap.filter(m, has(m.v2) && m.v2 == 'z').map(m, m.v2).size() == 1",
686
687
688
689 "self.listMap.exists(m, m.v2 == 'z')",
690 "!self.listMap.all(m, m.v2 != 'z')",
691 },
692 errors: map[string]string{
693
694
695
696
697 "self.listMap.all(m, m.v2 == 'z')": "no such key: v2",
698
699
700 "self.listMap.exists_one(m, m.v2 == 'z')": "no such key: v2",
701
702 "self.listMap.filter(m, m.v2 == 'z').size() == 1": "no such key: v2",
703 "self.listMap.map(m, m.v2).size() == 1": "no such key: v2",
704
705 "self.listMap.map(m, m.v2 == 'z', m.v2).size() == 1": "no such key: v2",
706 },
707 },
708 {name: "list access",
709 obj: map[string]interface{}{
710 "array": []interface{}{1, 1, 2, 2, 3, 3, 4, 5},
711 },
712 schema: objectTypePtr(map[string]schema.Structural{
713 "array": listType(&integerType),
714 }),
715 valid: []string{
716 "2 in self.array",
717 "self.array.all(e, e > 0)",
718 "self.array.exists(e, e > 2)",
719 "self.array.exists_one(e, e > 4)",
720 "!self.array.all(e, e < 2)",
721 "!self.array.exists(e, e < 0)",
722 "!self.array.exists_one(e, e == 2)",
723 "self.array.all(e, e < 100)",
724 "size(self.array.filter(e, e%2 == 0)) == 3",
725 "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
726 "size(self.array) == 8",
727 },
728 errors: map[string]string{
729 "self.array[100] == 0": "index out of bounds: 100",
730 },
731 },
732 {name: "listSet access",
733 obj: map[string]interface{}{
734 "set": []interface{}{1, 2, 3, 4, 5},
735 },
736 schema: objectTypePtr(map[string]schema.Structural{
737 "set": listType(&integerType),
738 }),
739 valid: []string{
740 "3 in self.set",
741 "self.set.all(e, e > 0)",
742 "self.set.exists(e, e > 3)",
743 "self.set.exists_one(e, e == 3)",
744 "!self.set.all(e, e < 3)",
745 "!self.set.exists(e, e < 0)",
746 "!self.set.exists_one(e, e > 3)",
747 "self.set.all(e, e < 10)",
748 "size(self.set.filter(e, e%2 == 0)) == 2",
749 "self.set.map(e, e * 20).filter(e, e > 50).exists_one(e, e == 60)",
750 "size(self.set) == 5",
751 },
752 },
753 {name: "typemeta and objectmeta access specified",
754 obj: map[string]interface{}{
755 "apiVersion": "v1",
756 "kind": "Pod",
757 "metadata": map[string]interface{}{
758 "name": "foo",
759 "generateName": "pickItForMe",
760 "namespace": "xyz",
761 },
762 },
763 schema: objectTypePtr(map[string]schema.Structural{
764 "kind": stringType,
765 "apiVersion": stringType,
766 "metadata": objectType(map[string]schema.Structural{
767 "name": stringType,
768 "generateName": stringType,
769 }),
770 }),
771 valid: []string{
772 "self.kind == 'Pod'",
773 "self.apiVersion == 'v1'",
774 "self.metadata.name == 'foo'",
775 "self.metadata.generateName == 'pickItForMe'",
776 },
777 errors: map[string]string{
778 "has(self.metadata.namespace)": "undefined field 'namespace'",
779 },
780 },
781 {name: "typemeta and objectmeta access not specified",
782 isRoot: true,
783 obj: map[string]interface{}{
784 "apiVersion": "v1",
785 "kind": "Pod",
786 "metadata": map[string]interface{}{
787 "name": "foo",
788 "generateName": "pickItForMe",
789 "namespace": "xyz",
790 },
791 "spec": map[string]interface{}{
792 "field1": "a",
793 },
794 },
795 schema: objectTypePtr(map[string]schema.Structural{
796 "spec": objectType(map[string]schema.Structural{
797 "field1": stringType,
798 }),
799 }),
800 valid: []string{
801 "self.kind == 'Pod'",
802 "self.apiVersion == 'v1'",
803 "self.metadata.name == 'foo'",
804 "self.metadata.generateName == 'pickItForMe'",
805 "self.spec.field1 == 'a'",
806 },
807 errors: map[string]string{
808 "has(self.metadata.namespace)": "undefined field 'namespace'",
809 },
810 },
811
812
813 {name: "embedded object",
814 obj: map[string]interface{}{
815 "embedded": map[string]interface{}{
816 "apiVersion": "v1",
817 "kind": "Pod",
818 "metadata": map[string]interface{}{
819 "name": "foo",
820 "generateName": "pickItForMe",
821 "namespace": "xyz",
822 },
823 "spec": map[string]interface{}{
824 "field1": "a",
825 },
826 },
827 },
828 schema: objectTypePtr(map[string]schema.Structural{
829 "embedded": {
830 Generic: schema.Generic{Type: "object"},
831 Extensions: schema.Extensions{
832 XEmbeddedResource: true,
833 },
834 },
835 }),
836 valid: []string{
837
838
839 "self.embedded.kind == 'Pod'",
840 "self.embedded.apiVersion == 'v1'",
841 "self.embedded.metadata.name == 'foo'",
842 "self.embedded.metadata.generateName == 'pickItForMe'",
843 },
844
845 errors: map[string]string{
846 "has(self.embedded.metadata.namespace)": "undefined field 'namespace'",
847 "has(self.embedded.spec)": "undefined field 'spec'",
848 },
849 },
850 {name: "embedded object with properties",
851 obj: map[string]interface{}{
852 "embedded": map[string]interface{}{
853 "apiVersion": "v1",
854 "kind": "Pod",
855 "metadata": map[string]interface{}{
856 "name": "foo",
857 "generateName": "pickItForMe",
858 "namespace": "xyz",
859 },
860 "spec": map[string]interface{}{
861 "field1": "a",
862 },
863 },
864 },
865 schema: objectTypePtr(map[string]schema.Structural{
866 "embedded": {
867 Generic: schema.Generic{Type: "object"},
868 Extensions: schema.Extensions{
869 XEmbeddedResource: true,
870 },
871 Properties: map[string]schema.Structural{
872 "kind": stringType,
873 "apiVersion": stringType,
874 "metadata": objectType(map[string]schema.Structural{
875 "name": stringType,
876 "generateName": stringType,
877 }),
878 "spec": objectType(map[string]schema.Structural{
879 "field1": stringType,
880 }),
881 },
882 },
883 }),
884 valid: []string{
885
886
887 "self.embedded.kind == 'Pod'",
888 "self.embedded.apiVersion == 'v1'",
889 "self.embedded.metadata.name == 'foo'",
890 "self.embedded.metadata.generateName == 'pickItForMe'",
891
892 "self.embedded.spec.field1 == 'a'",
893 },
894 errors: map[string]string{
895
896 "has(self.embedded.metadata.namespace)": "undefined field 'namespace'",
897 },
898 },
899 {name: "embedded object with preserve unknown",
900 obj: map[string]interface{}{
901 "embedded": map[string]interface{}{
902 "apiVersion": "v1",
903 "kind": "Pod",
904 "metadata": map[string]interface{}{
905 "name": "foo",
906 "generateName": "pickItForMe",
907 "namespace": "xyz",
908 },
909 "spec": map[string]interface{}{
910 "field1": "a",
911 },
912 },
913 },
914 schema: objectTypePtr(map[string]schema.Structural{
915 "embedded": {
916 Generic: schema.Generic{Type: "object"},
917 Extensions: schema.Extensions{
918 XPreserveUnknownFields: true,
919 XEmbeddedResource: true,
920 },
921 },
922 }),
923 valid: []string{
924
925
926 "self.embedded.kind == 'Pod'",
927 "self.embedded.apiVersion == 'v1'",
928 "self.embedded.metadata.name == 'foo'",
929 "self.embedded.metadata.generateName == 'pickItForMe'",
930
931
932 "has(self.embedded)",
933 },
934
935
936 errors: map[string]string{
937 "has(self.embedded.spec)": "undefined field 'spec'",
938 },
939 },
940 {name: "string in intOrString",
941 obj: map[string]interface{}{
942 "something": "25%",
943 },
944 schema: objectTypePtr(map[string]schema.Structural{
945 "something": intOrStringType(),
946 }),
947 valid: []string{
948
949
950 "self.something == '25%'",
951 "self.something != 1",
952 "self.something == 1 || self.something == '25%'",
953 "self.something == '25%' || self.something == 1",
954
955
956
957 "type(self.something) == string && self.something == '25%'",
958 "type(self.something) == int ? self.something == 1 : self.something == '25%'",
959
960
961
962 "self.something != ['anything']",
963 },
964 },
965 {name: "int in intOrString",
966 obj: map[string]interface{}{
967 "something": int64(1),
968 },
969 schema: objectTypePtr(map[string]schema.Structural{
970 "something": intOrStringType(),
971 }),
972 valid: []string{
973
974
975 "self.something == 1",
976 "self.something != 'some string'",
977 "self.something == 1 || self.something == '25%'",
978 "self.something == '25%' || self.something == 1",
979
980
981
982 "type(self.something) == int && self.something == 1",
983 "type(self.something) == int ? self.something == 1 : self.something == '25%'",
984
985
986
987 "self.something != ['anything']",
988 },
989 },
990 {name: "null in intOrString",
991 obj: map[string]interface{}{
992 "something": nil,
993 },
994 schema: objectTypePtr(map[string]schema.Structural{
995 "something": withNullable(true, intOrStringType()),
996 }),
997 valid: []string{
998 "!has(self.something)",
999 },
1000 errors: map[string]string{
1001 "type(self.something) == int": "no such key",
1002 },
1003 },
1004 {name: "percent comparison using intOrString",
1005 obj: map[string]interface{}{
1006 "min": "50%",
1007 "current": 5,
1008 "available": 10,
1009 },
1010 schema: objectTypePtr(map[string]schema.Structural{
1011 "min": intOrStringType(),
1012 "current": integerType,
1013 "available": integerType,
1014 }),
1015 valid: []string{
1016
1017 `type(self.min) == string && self.min.matches(r'(\d+(\.\d+)?%)')`,
1018
1019 "type(self.min) == int ? self.current <= self.min : double(self.current) / double(self.available) >= double(self.min.replace('%', '')) / 100.0",
1020 },
1021 },
1022 {name: "preserve unknown fields",
1023 obj: map[string]interface{}{
1024 "withUnknown": map[string]interface{}{
1025 "field1": "a",
1026 "field2": "b",
1027 },
1028 "withUnknownList": []interface{}{
1029 map[string]interface{}{
1030 "field1": "a",
1031 "field2": "b",
1032 },
1033 map[string]interface{}{
1034 "field1": "x",
1035 "field2": "y",
1036 },
1037 map[string]interface{}{
1038 "field1": "x",
1039 "field2": "y",
1040 },
1041 map[string]interface{}{},
1042 map[string]interface{}{},
1043 },
1044 "withUnknownFieldList": []interface{}{
1045 map[string]interface{}{
1046 "fieldOfUnknownType": "a",
1047 },
1048 map[string]interface{}{
1049 "fieldOfUnknownType": 1,
1050 },
1051 map[string]interface{}{
1052 "fieldOfUnknownType": 1,
1053 },
1054 },
1055 "anyvalList": []interface{}{"a", 2},
1056 "anyvalMap": map[string]interface{}{"k": "1"},
1057 "anyvalField1": 1,
1058 "anyvalField2": "a",
1059 },
1060 schema: objectTypePtr(map[string]schema.Structural{
1061 "withUnknown": {
1062 Generic: schema.Generic{Type: "object"},
1063 Extensions: schema.Extensions{
1064 XPreserveUnknownFields: true,
1065 },
1066 },
1067 "withUnknownList": listType(&schema.Structural{
1068 Generic: schema.Generic{Type: "object"},
1069 Extensions: schema.Extensions{
1070 XPreserveUnknownFields: true,
1071 },
1072 }),
1073 "withUnknownFieldList": listType(&schema.Structural{
1074 Generic: schema.Generic{Type: "object"},
1075 Properties: map[string]schema.Structural{
1076 "fieldOfUnknownType": {
1077 Extensions: schema.Extensions{
1078 XPreserveUnknownFields: true,
1079 },
1080 },
1081 },
1082 }),
1083 "anyvalList": listType(&schema.Structural{
1084 Extensions: schema.Extensions{
1085 XPreserveUnknownFields: true,
1086 },
1087 }),
1088 "anyvalMap": mapType(&schema.Structural{
1089 Extensions: schema.Extensions{
1090 XPreserveUnknownFields: true,
1091 },
1092 }),
1093 "anyvalField1": {
1094 Extensions: schema.Extensions{
1095 XPreserveUnknownFields: true,
1096 },
1097 },
1098 "anyvalField2": {
1099 Extensions: schema.Extensions{
1100 XPreserveUnknownFields: true,
1101 },
1102 },
1103 }),
1104 valid: []string{
1105 "has(self.withUnknown)",
1106 "self.withUnknownList.size() == 5",
1107
1108 "self.withUnknownList[0] != self.withUnknownList[1]",
1109 "self.withUnknownList[1] == self.withUnknownList[2]",
1110 "self.withUnknownList[3] == self.withUnknownList[4]",
1111
1112
1113 "self.withUnknownFieldList[0] != self.withUnknownFieldList[1]",
1114 "self.withUnknownFieldList[1] == self.withUnknownFieldList[2]",
1115 },
1116 errors: map[string]string{
1117
1118 "has(self.withUnknown.field1)": "undefined field 'field1'",
1119
1120
1121 "has(self.anyvalList)": "undefined field 'anyvalList'",
1122 "has(self.anyvalMap)": "undefined field 'anyvalMap'",
1123 "has(self.anyvalField1)": "undefined field 'anyvalField1'",
1124 "has(self.anyvalField2)": "undefined field 'anyvalField2'",
1125 },
1126 },
1127 {name: "known and unknown fields",
1128 obj: map[string]interface{}{
1129 "withUnknown": map[string]interface{}{
1130 "known": 1,
1131 "unknown": "a",
1132 },
1133 "withUnknownList": []interface{}{
1134 map[string]interface{}{
1135 "known": 1,
1136 "unknown": "a",
1137 },
1138 map[string]interface{}{
1139 "known": 1,
1140 "unknown": "b",
1141 },
1142 map[string]interface{}{
1143 "known": 1,
1144 "unknown": "b",
1145 },
1146 map[string]interface{}{
1147 "known": 1,
1148 },
1149 map[string]interface{}{
1150 "known": 1,
1151 },
1152 map[string]interface{}{
1153 "known": 2,
1154 },
1155 },
1156 },
1157 schema: &schema.Structural{
1158 Generic: schema.Generic{
1159 Type: "object",
1160 },
1161 Properties: map[string]schema.Structural{
1162 "withUnknown": {
1163 Generic: schema.Generic{Type: "object"},
1164 Extensions: schema.Extensions{
1165 XPreserveUnknownFields: true,
1166 },
1167 Properties: map[string]schema.Structural{
1168 "known": integerType,
1169 },
1170 },
1171 "withUnknownList": listType(&schema.Structural{
1172 Generic: schema.Generic{Type: "object"},
1173 Extensions: schema.Extensions{
1174 XPreserveUnknownFields: true,
1175 },
1176 Properties: map[string]schema.Structural{
1177 "known": integerType,
1178 },
1179 }),
1180 },
1181 },
1182 valid: []string{
1183 "self.withUnknown.known == 1",
1184 "self.withUnknownList[0] != self.withUnknownList[1]",
1185
1186 "self.withUnknownList[1] == self.withUnknownList[2]",
1187
1188
1189 "self.withUnknownList[0] != self.withUnknownList[1]",
1190 "self.withUnknownList[0] != self.withUnknownList[3]",
1191 "self.withUnknownList[0] != self.withUnknownList[5]",
1192
1193
1194 "self.withUnknownList[3] == self.withUnknownList[4]",
1195 "self.withUnknownList[4] != self.withUnknownList[5]",
1196 },
1197
1198 errors: map[string]string{
1199 "has(self.withUnknown.unknown)": "undefined field 'unknown'",
1200 },
1201 },
1202 {name: "field nullability",
1203 obj: map[string]interface{}{
1204 "setPlainStr": "v1",
1205 "setDefaultedStr": "v2",
1206 "setNullableStr": "v3",
1207 "setToNullNullableStr": nil,
1208
1209
1210
1211 "unsetDefaultedStr": "default value",
1212 },
1213 schema: objectTypePtr(map[string]schema.Structural{
1214 "unsetPlainStr": stringType,
1215 "unsetDefaultedStr": withDefault("default value", stringType),
1216 "unsetNullableStr": withNullable(true, stringType),
1217
1218 "setPlainStr": stringType,
1219 "setDefaultedStr": withDefault("default value", stringType),
1220 "setNullableStr": withNullable(true, stringType),
1221 "setToNullNullableStr": withNullable(true, stringType),
1222 }),
1223 valid: []string{
1224 "!has(self.unsetPlainStr)",
1225 "has(self.unsetDefaultedStr) && self.unsetDefaultedStr == 'default value'",
1226 "!has(self.unsetNullableStr)",
1227
1228 "has(self.setPlainStr) && self.setPlainStr == 'v1'",
1229 "has(self.setDefaultedStr) && self.setDefaultedStr == 'v2'",
1230 "has(self.setNullableStr) && self.setNullableStr == 'v3'",
1231
1232
1233 "type(self.setNullableStr) != null_type",
1234
1235
1236 "!has(self.setToNullNullableStr)",
1237 },
1238 errors: map[string]string{
1239
1240
1241 "self.unsetPlainStr == null": "no matching overload for '_==_' applied to '(string, null)",
1242 "self.unsetDefaultedStr != null": "no matching overload for '_!=_' applied to '(string, null)",
1243 "self.unsetNullableStr == null": "no matching overload for '_==_' applied to '(string, null)",
1244 "self.setPlainStr != null": "no matching overload for '_!=_' applied to '(string, null)",
1245 "self.setDefaultedStr != null": "no matching overload for '_!=_' applied to '(string, null)",
1246 "self.setNullableStr != null": "no matching overload for '_!=_' applied to '(string, null)",
1247 },
1248 },
1249 {name: "null values in container types",
1250 obj: map[string]interface{}{
1251 "m": map[string]interface{}{
1252 "a": nil,
1253 "b": "not-nil",
1254 },
1255 "l": []interface{}{
1256 nil, "not-nil",
1257 },
1258 "s": []interface{}{
1259 nil, "not-nil",
1260 },
1261 },
1262 schema: objectTypePtr(map[string]schema.Structural{
1263 "m": mapType(withNullablePtr(true, stringType)),
1264 "l": listType(withNullablePtr(true, stringType)),
1265 "s": listSetType(withNullablePtr(true, stringType)),
1266 }),
1267 valid: []string{
1268 "self.m.size() == 2",
1269 "'a' in self.m",
1270 "type(self.m['a']) == null_type",
1271
1272
1273 "self.l.size() == 2",
1274 "type(self.l[0]) == null_type",
1275
1276
1277 "self.s.size() == 2",
1278 "type(self.s[0]) == null_type",
1279
1280 },
1281 errors: map[string]string{
1282
1283
1284
1285
1286
1287
1288
1289 },
1290 },
1291 {name: "escaping",
1292 obj: map[string]interface{}{
1293
1294 "true": 1, "false": 2, "null": 3, "in": 4, "as": 5,
1295 "break": 6, "const": 7, "continue": 8, "else": 9,
1296 "for": 10, "function": 11, "if": 12, "import": 13,
1297 "let": 14, "loop": 15, "package": 16, "namespace": 17,
1298 "return": 18, "var": 19, "void": 20, "while": 21,
1299
1300 "int": 101, "uint": 102, "double": 103, "bool": 104,
1301 "string": 105, "bytes": 106, "list": 107, "map": 108,
1302 "null_type": 109, "type": 110,
1303
1304 "self": 201,
1305
1306 "getDate": 202,
1307 "all": 203,
1308 "size": "204",
1309
1310 "_true": 301,
1311
1312 "dot.dot": 401,
1313 "dash-dash": 402,
1314 "slash/slash": 403,
1315 "underscore_underscore": 404,
1316 "doubleunderscore__doubleunderscore": 405,
1317 },
1318 schema: objectTypePtr(map[string]schema.Structural{
1319 "true": integerType, "false": integerType, "null": integerType, "in": integerType, "as": integerType,
1320 "break": integerType, "const": integerType, "continue": integerType, "else": integerType,
1321 "for": integerType, "function": integerType, "if": integerType, "import": integerType,
1322 "let": integerType, "loop": integerType, "package": integerType, "namespace": integerType,
1323 "return": integerType, "var": integerType, "void": integerType, "while": integerType,
1324
1325 "int": integerType, "uint": integerType, "double": integerType, "bool": integerType,
1326 "string": integerType, "bytes": integerType, "list": integerType, "map": integerType,
1327 "null_type": integerType, "type": integerType,
1328
1329 "self": integerType,
1330
1331 "getDate": integerType,
1332 "all": integerType,
1333 "size": stringType,
1334
1335 "_true": integerType,
1336
1337 "dot.dot": integerType,
1338 "dash-dash": integerType,
1339 "slash/slash": integerType,
1340 "underscore_underscore": integerType,
1341 "doubleunderscore__doubleunderscore": integerType,
1342 }),
1343 valid: []string{
1344
1345 "self.__true__ == 1", "self.__false__ == 2", "self.__null__ == 3", "self.__in__ == 4", "self.__as__ == 5",
1346 "self.__break__ == 6", "self.__const__ == 7", "self.__continue__ == 8", "self.__else__ == 9",
1347 "self.__for__ == 10", "self.__function__ == 11", "self.__if__ == 12", "self.__import__ == 13",
1348 "self.__let__ == 14", "self.__loop__ == 15", "self.__package__ == 16", "self.__namespace__ == 17",
1349 "self.__return__ == 18", "self.__var__ == 19", "self.__void__ == 20", "self.__while__ == 21", "self.__true__ == 1",
1350
1351
1352
1353 "self.int == 101", "self.uint == 102", "self.double == 103", "self.bool == 104",
1354 "self.string == 105", "self.bytes == 106", "self.list == 107", "self.map == 108",
1355 "self.null_type == 109", "self.type == 110",
1356
1357
1358
1359 "self.self == 201",
1360
1361
1362 "self.getDate == 202",
1363 "self.all == 203",
1364 "self.size == '204'",
1365
1366 "self._true == 301",
1367 "self.__true__ != 301",
1368
1369 "self.dot__dot__dot == 401",
1370 "self.dash__dash__dash == 402",
1371 "self.slash__slash__slash == 403",
1372 "self.underscore_underscore == 404",
1373 "self.doubleunderscore__underscores__doubleunderscore == 405",
1374 },
1375 errors: map[string]string{
1376
1377 "self.true == 1": "mismatched input 'true' expecting IDENTIFIER",
1378
1379 "self == 201": "found no matching overload for '_==_'",
1380
1381
1382 "self.__illegal__ == 301": "undefined field '__illegal__'",
1383 },
1384 },
1385 {name: "map keys are not escaped",
1386 obj: map[string]interface{}{
1387 "m": map[string]interface{}{
1388 "@": 1,
1389 "9": 2,
1390 "int": 3,
1391 "./-": 4,
1392 "π": 5,
1393 },
1394 },
1395 schema: objectTypePtr(map[string]schema.Structural{"m": mapType(&integerType)}),
1396 valid: []string{
1397 "self.m['@'] == 1",
1398 "self.m['9'] == 2",
1399 "self.m['int'] == 3",
1400 "self.m['./-'] == 4",
1401 "self.m['π'] == 5",
1402 },
1403 },
1404 {name: "object types are not accessible",
1405 obj: map[string]interface{}{
1406 "nestedInMap": map[string]interface{}{
1407 "k1": map[string]interface{}{
1408 "inMapField": 1,
1409 },
1410 "k2": map[string]interface{}{
1411 "inMapField": 2,
1412 },
1413 },
1414 "nestedInList": []interface{}{
1415 map[string]interface{}{
1416 "inListField": 1,
1417 },
1418 map[string]interface{}{
1419 "inListField": 2,
1420 },
1421 },
1422 },
1423 schema: objectTypePtr(map[string]schema.Structural{
1424 "nestedInMap": mapType(objectTypePtr(map[string]schema.Structural{
1425 "inMapField": integerType,
1426 })),
1427 "nestedInList": listType(objectTypePtr(map[string]schema.Structural{
1428 "inListField": integerType,
1429 })),
1430 }),
1431 valid: []string{
1432
1433
1434
1435 "type(self) == type(self)",
1436 "type(self.nestedInMap['k1']) == type(self.nestedInMap['k2'])",
1437 "type(self.nestedInList[0]) == type(self.nestedInList[1])",
1438 },
1439 errors: map[string]string{
1440
1441
1442
1443
1444 "string(type(self.nestedInList[0])).endsWith('.nestedInList.@idx')": "found no matching overload for 'string' applied to '(type",
1445 },
1446 },
1447 {name: "listMaps with unsupported identity characters in property names",
1448 obj: map[string]interface{}{
1449 "objs": []interface{}{
1450 []interface{}{
1451 map[string]interface{}{"k!": "a", "k.": "1"},
1452 map[string]interface{}{"k!": "b", "k.": "2"},
1453 },
1454 []interface{}{
1455 map[string]interface{}{"k!": "b", "k.": "2"},
1456 map[string]interface{}{"k!": "a", "k.": "1"},
1457 },
1458 []interface{}{
1459 map[string]interface{}{"k!": "b", "k.": "2"},
1460 map[string]interface{}{"k!": "c", "k.": "1"},
1461 },
1462 []interface{}{
1463 map[string]interface{}{"k!": "b", "k.": "2"},
1464 map[string]interface{}{"k!": "a", "k.": "3"},
1465 },
1466 },
1467 },
1468 schema: objectTypePtr(map[string]schema.Structural{
1469 "objs": listType(listMapTypePtr([]string{"k!", "k."}, objectTypePtr(map[string]schema.Structural{
1470 "k!": stringType,
1471 "k.": stringType,
1472 }))),
1473 }),
1474 valid: []string{
1475 "self.objs[0] == self.objs[1]",
1476 "self.objs[0][0].k__dot__ == '1'",
1477 "self.objs[0] != self.objs[2]",
1478 "self.objs[0] != self.objs[3]",
1479 },
1480 errors: map[string]string{
1481
1482 "self.objs[0][0].k! == '1'": "Syntax error: mismatched input '!' expecting",
1483 },
1484 },
1485 {name: "container type composition",
1486 obj: map[string]interface{}{
1487 "obj": map[string]interface{}{
1488 "field": "a",
1489 },
1490 "mapOfMap": map[string]interface{}{
1491 "x": map[string]interface{}{
1492 "y": "b",
1493 },
1494 },
1495 "mapOfObj": map[string]interface{}{
1496 "k": map[string]interface{}{
1497 "field2": "c",
1498 },
1499 },
1500 "mapOfListMap": map[string]interface{}{
1501 "o": []interface{}{
1502 map[string]interface{}{
1503 "k": "1",
1504 "v": "d",
1505 },
1506 },
1507 },
1508 "mapOfList": map[string]interface{}{
1509 "l": []interface{}{"e"},
1510 },
1511 "listMapOfObj": []interface{}{
1512 map[string]interface{}{
1513 "k2": "2",
1514 "v2": "f",
1515 },
1516 },
1517 "listOfMap": []interface{}{
1518 map[string]interface{}{
1519 "z": "g",
1520 },
1521 },
1522 "listOfObj": []interface{}{
1523 map[string]interface{}{
1524 "field3": "h",
1525 },
1526 },
1527 "listOfListMap": []interface{}{
1528 []interface{}{
1529 map[string]interface{}{
1530 "k3": "3",
1531 "v3": "i",
1532 },
1533 },
1534 },
1535 },
1536 schema: objectTypePtr(map[string]schema.Structural{
1537 "obj": objectType(map[string]schema.Structural{
1538 "field": stringType,
1539 }),
1540 "mapOfMap": mapType(mapTypePtr(&stringType)),
1541 "mapOfObj": mapType(objectTypePtr(map[string]schema.Structural{
1542 "field2": stringType,
1543 })),
1544 "mapOfListMap": mapType(listMapTypePtr([]string{"k"}, objectTypePtr(map[string]schema.Structural{
1545 "k": stringType,
1546 "v": stringType,
1547 }))),
1548 "mapOfList": mapType(listTypePtr(&stringType)),
1549 "listMapOfObj": listMapType([]string{"k2"}, objectTypePtr(map[string]schema.Structural{
1550 "k2": stringType,
1551 "v2": stringType,
1552 })),
1553 "listOfMap": listType(mapTypePtr(&stringType)),
1554 "listOfObj": listType(objectTypePtr(map[string]schema.Structural{
1555 "field3": stringType,
1556 })),
1557 "listOfListMap": listType(listMapTypePtr([]string{"k3"}, objectTypePtr(map[string]schema.Structural{
1558 "k3": stringType,
1559 "v3": stringType,
1560 }))),
1561 }),
1562 valid: []string{
1563 "self.obj.field == 'a'",
1564 "self.mapOfMap['x']['y'] == 'b'",
1565 "self.mapOfObj['k'].field2 == 'c'",
1566 "self.mapOfListMap['o'].exists(e, e.k == '1' && e.v == 'd')",
1567 "self.mapOfList['l'][0] == 'e'",
1568 "self.listMapOfObj.exists(e, e.k2 == '2' && e.v2 == 'f')",
1569 "self.listOfMap[0]['z'] == 'g'",
1570 "self.listOfObj[0].field3 == 'h'",
1571 "self.listOfListMap[0].exists(e, e.k3 == '3' && e.v3 == 'i')",
1572 },
1573 errors: map[string]string{},
1574 },
1575 {name: "invalid data",
1576 obj: map[string]interface{}{
1577 "o": []interface{}{},
1578 "m": []interface{}{},
1579 "l": map[string]interface{}{},
1580 "s": map[string]interface{}{},
1581 "lm": map[string]interface{}{},
1582 "intorstring": true,
1583 "nullable": []interface{}{nil},
1584 },
1585 schema: objectTypePtr(map[string]schema.Structural{
1586 "o": objectType(map[string]schema.Structural{
1587 "field": stringType,
1588 }),
1589 "m": mapType(&stringType),
1590 "l": listType(&stringType),
1591 "s": listSetType(&stringType),
1592 "lm": listMapType([]string{"k"}, objectTypePtr(map[string]schema.Structural{
1593 "k": stringType,
1594 "v": stringType,
1595 })),
1596 "intorstring": intOrStringType(),
1597 "nullable": listType(&stringType),
1598 }),
1599 errors: map[string]string{
1600
1601 "has(self.o)": "invalid data, expected a map for the provided schema with type=object",
1602 "has(self.m)": "invalid data, expected a map for the provided schema with type=object",
1603 "has(self.l)": "invalid data, expected an array for the provided schema with type=array",
1604 "has(self.s)": "invalid data, expected an array for the provided schema with type=array",
1605 "has(self.lm)": "invalid data, expected an array for the provided schema with type=array",
1606 "has(self.intorstring)": "invalid data, expected XIntOrString value to be either a string or integer",
1607 "self.nullable[0] == 'x'": "invalid data, got null for schema with nullable=false",
1608
1609 },
1610 },
1611 {name: "stdlib list functions",
1612 obj: map[string]interface{}{
1613 "ints": []interface{}{int64(1), int64(2), int64(2), int64(3)},
1614 "unsortedInts": []interface{}{int64(2), int64(1)},
1615 "emptyInts": []interface{}{},
1616
1617 "doubles": []interface{}{float64(1), float64(2), float64(2), float64(3)},
1618 "unsortedDoubles": []interface{}{float64(2), float64(1)},
1619 "emptyDoubles": []interface{}{},
1620
1621 "intBackedDoubles": []interface{}{int64(1), int64(2), int64(2), int64(3)},
1622 "unsortedIntBackedDDoubles": []interface{}{int64(2), int64(1)},
1623 "emptyIntBackedDDoubles": []interface{}{},
1624
1625 "durations": []interface{}{"1s", "1m", "1m", "1h"},
1626 "unsortedDurations": []interface{}{"1m", "1s"},
1627 "emptyDurations": []interface{}{},
1628
1629 "strings": []interface{}{"a", "b", "b", "c"},
1630 "unsortedStrings": []interface{}{"b", "a"},
1631 "emptyStrings": []interface{}{},
1632
1633 "dates": []interface{}{"2000-01-01", "2000-02-01", "2000-02-01", "2010-01-01"},
1634 "unsortedDates": []interface{}{"2000-02-01", "2000-01-01"},
1635 "emptyDates": []interface{}{},
1636
1637 "objs": []interface{}{
1638 map[string]interface{}{"f1": "a", "f2": "a"},
1639 map[string]interface{}{"f1": "a", "f2": "b"},
1640 map[string]interface{}{"f1": "a", "f2": "b"},
1641 map[string]interface{}{"f1": "a", "f2": "c"},
1642 },
1643 },
1644 schema: objectTypePtr(map[string]schema.Structural{
1645 "ints": listType(&integerType),
1646 "unsortedInts": listType(&integerType),
1647 "emptyInts": listType(&integerType),
1648
1649 "doubles": listType(&doubleType),
1650 "unsortedDoubles": listType(&doubleType),
1651 "emptyDoubles": listType(&doubleType),
1652
1653 "intBackedDoubles": listType(&doubleType),
1654 "unsortedIntBackedDDoubles": listType(&doubleType),
1655 "emptyIntBackedDDoubles": listType(&doubleType),
1656
1657 "durations": listType(&durationFormat),
1658 "unsortedDurations": listType(&durationFormat),
1659 "emptyDurations": listType(&durationFormat),
1660
1661 "strings": listType(&stringType),
1662 "unsortedStrings": listType(&stringType),
1663 "emptyStrings": listType(&stringType),
1664
1665 "dates": listType(&dateFormat),
1666 "unsortedDates": listType(&dateFormat),
1667 "emptyDates": listType(&dateFormat),
1668
1669 "objs": listType(objectTypePtr(map[string]schema.Structural{
1670 "f1": stringType,
1671 "f2": stringType,
1672 })),
1673 }),
1674 valid: []string{
1675 "self.ints.sum() == 8",
1676 "self.ints.min() == 1",
1677 "self.ints.max() == 3",
1678 "self.emptyInts.sum() == 0",
1679 "self.ints.isSorted()",
1680 "self.emptyInts.isSorted()",
1681 "self.unsortedInts.isSorted() == false",
1682 "self.ints.indexOf(2) == 1",
1683 "self.ints.lastIndexOf(2) == 2",
1684 "self.ints.indexOf(10) == -1",
1685 "self.ints.lastIndexOf(10) == -1",
1686
1687 "self.doubles.sum() == 8.0",
1688 "self.doubles.min() == 1.0",
1689 "self.doubles.max() == 3.0",
1690 "self.emptyDoubles.sum() == 0.0",
1691 "self.doubles.isSorted()",
1692 "self.emptyDoubles.isSorted()",
1693 "self.unsortedDoubles.isSorted() == false",
1694 "self.doubles.indexOf(2.0) == 1",
1695 "self.doubles.lastIndexOf(2.0) == 2",
1696 "self.doubles.indexOf(10.0) == -1",
1697 "self.doubles.lastIndexOf(10.0) == -1",
1698
1699 "self.intBackedDoubles.sum() == 8.0",
1700 "self.intBackedDoubles.min() == 1.0",
1701 "self.intBackedDoubles.max() == 3.0",
1702 "self.emptyIntBackedDDoubles.sum() == 0.0",
1703 "self.intBackedDoubles.isSorted()",
1704 "self.emptyDoubles.isSorted()",
1705 "self.unsortedIntBackedDDoubles.isSorted() == false",
1706 "self.intBackedDoubles.indexOf(2.0) == 1",
1707 "self.intBackedDoubles.lastIndexOf(2.0) == 2",
1708 "self.intBackedDoubles.indexOf(10.0) == -1",
1709 "self.intBackedDoubles.lastIndexOf(10.0) == -1",
1710
1711 "self.durations.sum() == duration('1h2m1s')",
1712 "self.durations.min() == duration('1s')",
1713 "self.durations.max() == duration('1h')",
1714 "self.emptyDurations.sum() == duration('0')",
1715 "self.durations.isSorted()",
1716 "self.emptyDurations.isSorted()",
1717 "self.unsortedDurations.isSorted() == false",
1718 "self.durations.indexOf(duration('1m')) == 1",
1719 "self.durations.lastIndexOf(duration('1m')) == 2",
1720 "self.durations.indexOf(duration('2m')) == -1",
1721 "self.durations.lastIndexOf(duration('2m')) == -1",
1722
1723 "self.strings.min() == 'a'",
1724 "self.strings.max() == 'c'",
1725 "self.strings.isSorted()",
1726 "self.emptyStrings.isSorted()",
1727 "self.unsortedStrings.isSorted() == false",
1728 "self.strings.indexOf('b') == 1",
1729 "self.strings.lastIndexOf('b') == 2",
1730 "self.strings.indexOf('x') == -1",
1731 "self.strings.lastIndexOf('x') == -1",
1732
1733 "self.dates.min() == timestamp('2000-01-01T00:00:00.000Z')",
1734 "self.dates.max() == timestamp('2010-01-01T00:00:00.000Z')",
1735 "self.dates.isSorted()",
1736 "self.emptyDates.isSorted()",
1737 "self.unsortedDates.isSorted() == false",
1738 "self.dates.indexOf(timestamp('2000-02-01T00:00:00.000Z')) == 1",
1739 "self.dates.lastIndexOf(timestamp('2000-02-01T00:00:00.000Z')) == 2",
1740 "self.dates.indexOf(timestamp('2005-02-01T00:00:00.000Z')) == -1",
1741 "self.dates.lastIndexOf(timestamp('2005-02-01T00:00:00.000Z')) == -1",
1742
1743
1744 "[[1], [2]].indexOf([1]) == 0",
1745 "[{'a': 1}, {'b': 2}].lastIndexOf({'b': 2}) == 1",
1746 "self.objs.indexOf(self.objs[1]) == 1",
1747 "self.objs.lastIndexOf(self.objs[1]) == 2",
1748
1749
1750 "([0] + self.emptyInts).min() == 0",
1751
1752
1753 "dyn([]).sum() == 0",
1754 "dyn([1, 2]).sum() == 3",
1755 "dyn([1.0, 2.0]).sum() == 3.0",
1756
1757 "[].sum() == 0",
1758 },
1759 errors: map[string]string{
1760
1761 "self.emptyInts.min() == 1": "min called on empty list",
1762 "self.emptyInts.max() == 3": "max called on empty list",
1763 "self.emptyDoubles.min() == 1.0": "min called on empty list",
1764 "self.emptyDoubles.max() == 3.0": "max called on empty list",
1765 "self.emptyStrings.min() == 'a'": "min called on empty list",
1766 "self.emptyStrings.max() == 'c'": "max called on empty list",
1767
1768
1769 "['a', 'b'].sum() == 'c'": "found no matching overload for 'sum' applied to 'list(string).()",
1770
1771
1772 "[[1], [2]].min() == [1]": "found no matching overload for 'min' applied to 'list(list(int)).()",
1773 "[{'a': 1}, {'b': 2}].max() == {'b': 2}": "found no matching overload for 'max' applied to 'list(map(string, int)).()",
1774 },
1775 },
1776 {name: "stdlib regex functions",
1777 obj: map[string]interface{}{
1778 "str": "this is a 123 string 456",
1779 },
1780 schema: objectTypePtr(map[string]schema.Structural{
1781 "str": stringType,
1782 }),
1783 valid: []string{
1784 "self.str.find('[0-9]+') == '123'",
1785 "self.str.find('[0-9]+') != '456'",
1786 "self.str.find('xyz') == ''",
1787
1788 "self.str.findAll('[0-9]+') == ['123', '456']",
1789 "self.str.findAll('[0-9]+', 0) == []",
1790 "self.str.findAll('[0-9]+', 1) == ['123']",
1791 "self.str.findAll('[0-9]+', 2) == ['123', '456']",
1792 "self.str.findAll('[0-9]+', 3) == ['123', '456']",
1793 "self.str.findAll('[0-9]+', -1) == ['123', '456']",
1794 "self.str.findAll('xyz') == []",
1795 "self.str.findAll('xyz', 1) == []",
1796 },
1797 errors: map[string]string{
1798
1799 "self.str.find(')') == ''": "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
1800 "self.str.findAll(')') == []": "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
1801 "self.str.findAll(')', 1) == []": "compile error: program instantiation failed: error parsing regexp: unexpected ): `)`",
1802 "self.str.matches('x++')": "invalid matches argument",
1803 },
1804 },
1805 {name: "URL parsing",
1806 obj: map[string]interface{}{
1807 "url": "https://user:pass@kubernetes.io:80/docs/home?k1=a&k2=b&k2=c#anchor",
1808 },
1809 schema: objectTypePtr(map[string]schema.Structural{
1810 "url": stringType,
1811 }),
1812 valid: []string{
1813 "url('/path').getScheme() == ''",
1814 "url('https://example.com/').getScheme() == 'https'",
1815 "url('https://example.com:80/').getHost() == 'example.com:80'",
1816 "url('https://example.com/').getHost() == 'example.com'",
1817 "url('https://[::1]:80/').getHost() == '[::1]:80'",
1818 "url('https://[::1]/').getHost() == '[::1]'",
1819 "url('/path').getHost() == ''",
1820 "url('https://example.com:80/').getHostname() == 'example.com'",
1821 "url('https://127.0.0.1/').getHostname() == '127.0.0.1'",
1822 "url('https://[::1]/').getHostname() == '::1'",
1823 "url('/path').getHostname() == ''",
1824 "url('https://example.com:80/').getPort() == '80'",
1825 "url('https://example.com/').getPort() == ''",
1826 "url('/path').getPort() == ''",
1827 "url('https://example.com/path').getEscapedPath() == '/path'",
1828 "url('https://example.com/with space/').getEscapedPath() == '/with%20space/'",
1829 "url('https://example.com').getEscapedPath() == ''",
1830 "url('https://example.com/path?k1=a&k2=b&k2=c').getQuery() == { 'k1': ['a'], 'k2': ['b', 'c']}",
1831 "url('https://example.com/path?key with spaces=value with spaces').getQuery() == { 'key with spaces': ['value with spaces']}",
1832 "url('https://example.com/path?').getQuery() == {}",
1833 "url('https://example.com/path').getQuery() == {}",
1834
1835
1836 "url(self.url).getScheme() == 'https'",
1837 "url(self.url).getHost() == 'kubernetes.io:80'",
1838 "url(self.url).getHostname() == 'kubernetes.io'",
1839 "url(self.url).getPort() == '80'",
1840 "url(self.url).getEscapedPath() == '/docs/home'",
1841 "url(self.url).getQuery() == {'k1': ['a'], 'k2': ['b', 'c']}",
1842
1843 "isURL('https://user:pass@example.com:80/path?query=val#fragment')",
1844 "isURL('/path') == true",
1845 "isURL('https://a:b:c/') == false",
1846 "isURL('../relative-path') == false",
1847 },
1848 },
1849 {name: "transition rules",
1850 obj: map[string]interface{}{
1851 "v": "new",
1852 },
1853 oldObj: map[string]interface{}{
1854 "v": "old",
1855 },
1856 schema: objectTypePtr(map[string]schema.Structural{
1857 "v": stringType,
1858 }),
1859 valid: []string{
1860 "oldSelf.v != self.v",
1861 "oldSelf.v == 'old' && self.v == 'new'",
1862 },
1863 },
1864 {name: "skipped transition rule for nil old primitive",
1865 expectSkipped: true,
1866 obj: "exists",
1867 oldObj: nil,
1868 schema: &stringType,
1869 valid: []string{
1870 "oldSelf == self",
1871 },
1872 },
1873 {name: "skipped transition rule for nil old array",
1874 expectSkipped: true,
1875 obj: []interface{}{},
1876 oldObj: nil,
1877 schema: listTypePtr(&stringType),
1878 valid: []string{
1879 "oldSelf == self",
1880 },
1881 },
1882 {name: "skipped transition rule for nil old object",
1883 expectSkipped: true,
1884 obj: map[string]interface{}{"f": "exists"},
1885 oldObj: nil,
1886 schema: objectTypePtr(map[string]schema.Structural{
1887 "f": stringType,
1888 }),
1889 valid: []string{
1890 "oldSelf.f == self.f",
1891 },
1892 },
1893 {name: "skipped transition rule for old with non-nil interface but nil value",
1894 expectSkipped: true,
1895 obj: []interface{}{},
1896 oldObj: nilInterfaceOfStringSlice(),
1897 schema: listTypePtr(&stringType),
1898 valid: []string{
1899 "oldSelf == self",
1900 },
1901 },
1902 {name: "authorizer is not supported for CRD Validation Rules",
1903 obj: []interface{}{},
1904 oldObj: []interface{}{},
1905 schema: objectTypePtr(map[string]schema.Structural{}),
1906 errors: map[string]string{
1907 "authorizer.path('/healthz').check('get').allowed()": "undeclared reference to 'authorizer'",
1908 },
1909 },
1910 {name: "optionals",
1911 obj: map[string]interface{}{
1912 "presentObj": map[string]interface{}{
1913 "presentStr": "value",
1914 },
1915 "m": map[string]interface{}{"k": "v"},
1916 "l": []interface{}{"a"},
1917 },
1918 schema: objectTypePtr(map[string]schema.Structural{
1919 "presentObj": objectType(map[string]schema.Structural{
1920 "presentStr": stringType,
1921 }),
1922 "absentObj": objectType(map[string]schema.Structural{
1923 "absentStr": stringType,
1924 }),
1925 "m": mapType(&stringType),
1926 "l": listType(&stringType),
1927 }),
1928 valid: []string{
1929 "self.?presentObj.?presentStr == optional.of('value')",
1930 "self.presentObj.?presentStr == optional.of('value')",
1931 "self.presentObj.?presentStr.or(optional.of('nope')) == optional.of('value')",
1932 "self.presentObj.?presentStr.orValue('') == 'value'",
1933 "self.presentObj.?presentStr.hasValue() == true",
1934 "self.presentObj.?presentStr.optMap(v, v == 'value').hasValue()",
1935 "self.?absentObj.?absentStr == optional.none()",
1936 "self.?absentObj.?absentStr.or(optional.of('nope')) == optional.of('nope')",
1937 "self.?absentObj.?absentStr.orValue('nope') == 'nope'",
1938 "self.?absentObj.?absentStr.hasValue() == false",
1939 "self.?absentObj.?absentStr.optMap(v, v == 'value').hasValue() == false",
1940
1941 "self.m[?'k'] == optional.of('v')",
1942 "self.m[?'k'].or(optional.of('nope')) == optional.of('v')",
1943 "self.m[?'k'].orValue('') == 'v'",
1944 "self.m[?'k'].hasValue() == true",
1945 "self.m[?'k'].optMap(v, v == 'v').hasValue()",
1946 "self.m[?'x'] == optional.none()",
1947 "self.m[?'x'].or(optional.of('nope')) == optional.of('nope')",
1948 "self.m[?'x'].orValue('nope') == 'nope'",
1949 "self.m[?'x'].hasValue() == false",
1950 "self.m[?'x'].hasValue() == false",
1951
1952 "self.l[?0] == optional.of('a')",
1953 "self.l[?1] == optional.none()",
1954 "self.l[?0].orValue('') == 'a'",
1955 "self.l[?0].hasValue() == true",
1956 "self.l[?0].optMap(v, v == 'a').hasValue()",
1957 "self.l[?1] == optional.none()",
1958 "self.l[?1].or(optional.of('nope')) == optional.of('nope')",
1959 "self.l[?1].orValue('nope') == 'nope'",
1960 "self.l[?1].hasValue() == false",
1961 "self.l[?1].hasValue() == false",
1962
1963 "optional.ofNonZeroValue(1).hasValue()",
1964 "optional.ofNonZeroValue(uint(1)).hasValue()",
1965 "optional.ofNonZeroValue(1.1).hasValue()",
1966 "optional.ofNonZeroValue('a').hasValue()",
1967 "optional.ofNonZeroValue(true).hasValue()",
1968 "optional.ofNonZeroValue(['a']).hasValue()",
1969 "optional.ofNonZeroValue({'k': 'v'}).hasValue()",
1970 "optional.ofNonZeroValue(timestamp('2011-08-18T00:00:00.000+01:00')).hasValue()",
1971 "optional.ofNonZeroValue(duration('19h3m37s10ms')).hasValue()",
1972 "optional.ofNonZeroValue(null) == optional.none()",
1973 "optional.ofNonZeroValue(0) == optional.none()",
1974 "optional.ofNonZeroValue(uint(0)) == optional.none()",
1975 "optional.ofNonZeroValue(0.0) == optional.none()",
1976 "optional.ofNonZeroValue('') == optional.none()",
1977 "optional.ofNonZeroValue(false) == optional.none()",
1978 "optional.ofNonZeroValue([]) == optional.none()",
1979 "optional.ofNonZeroValue({}) == optional.none()",
1980 "optional.ofNonZeroValue(timestamp('0001-01-01T00:00:00.000+00:00')) == optional.none()",
1981 "optional.ofNonZeroValue(duration('0s')) == optional.none()",
1982
1983 "{?'k': optional.none(), 'k2': 'v2'} == {'k2': 'v2'}",
1984 "{?'k': optional.of('v'), 'k2': 'v2'} == {'k': 'v', 'k2': 'v2'}",
1985 "['a', ?optional.none(), 'c'] == ['a', 'c']",
1986 "['a', ?optional.of('v'), 'c'] == ['a', 'v', 'c']",
1987 },
1988 errors: map[string]string{
1989 "self.absentObj.?absentStr == optional.none()": "no such key: absentObj",
1990 },
1991 },
1992 {name: "quantity",
1993 obj: objs("20", "200M"),
1994 schema: schemas(stringType, stringType),
1995 valid: []string{
1996 "isQuantity(self.val1)",
1997 "isQuantity(self.val2)",
1998 `isQuantity("20Mi")`,
1999 `quantity(self.val2) == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
2000 `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
2001 `quantity(self.val1).isLessThan(quantity("100M"))`,
2002 `quantity(self.val2).isGreaterThan(quantity("50M"))`,
2003 `quantity(self.val2).compareTo(quantity("0.2G")) == 0`,
2004 `quantity("50k").add(quantity(self.val1)) == quantity("50.02k")`,
2005 `quantity("50k").sub(quantity(self.val1)) == quantity("49980")`,
2006 `quantity(self.val1).isInteger()`,
2007 },
2008 },
2009 }
2010
2011 for i := range tests {
2012 i := i
2013 t.Run(tests[i].name, func(t *testing.T) {
2014 t.Parallel()
2015 tt := tests[i]
2016 tt.costBudget = celconfig.RuntimeCELCostBudget
2017 ctx := context.TODO()
2018 for j := range tt.valid {
2019 validRule := tt.valid[j]
2020 testName := validRule
2021 if len(testName) > 127 {
2022 testName = testName[:127]
2023 }
2024 t.Run(testName, func(t *testing.T) {
2025 t.Parallel()
2026 s := withRule(*tt.schema, validRule)
2027 celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
2028 if celValidator == nil {
2029 t.Fatal("expected non nil validator")
2030 }
2031 errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
2032 for _, err := range errs {
2033 t.Errorf("unexpected error: %v", err)
2034 }
2035 if tt.expectSkipped {
2036
2037 if remainingBudget != tt.costBudget {
2038 t.Errorf("expected no cost expended for skipped validation, but got %d remaining from %d budget", remainingBudget, tt.costBudget)
2039 }
2040 return
2041 }
2042 })
2043 }
2044 for rule, expectErrToContain := range tt.errors {
2045 testName := rule
2046 if len(testName) > 127 {
2047 testName = testName[:127]
2048 }
2049 t.Run(testName, func(t *testing.T) {
2050 s := withRule(*tt.schema, rule)
2051 celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
2052 if celValidator == nil {
2053 t.Fatal("expected non nil validator")
2054 }
2055 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
2056 if len(errs) == 0 {
2057 t.Error("expected validation errors but got none")
2058 }
2059 for _, err := range errs {
2060 if err.Type != field.ErrorTypeInvalid || !strings.Contains(err.Error(), expectErrToContain) {
2061 t.Errorf("expected error to contain '%s', but got: %v", expectErrToContain, err)
2062 }
2063 }
2064 })
2065 }
2066 })
2067 }
2068 }
2069
2070
2071
2072 func TestValidationExpressionsAtSchemaLevels(t *testing.T) {
2073 tests := []struct {
2074 name string
2075 schema *schema.Structural
2076 obj interface{}
2077 oldObj interface{}
2078 errors []string
2079 }{
2080 {name: "invalid rule under array items",
2081 obj: map[string]interface{}{
2082 "f": []interface{}{1},
2083 },
2084 schema: objectTypePtr(map[string]schema.Structural{
2085 "f": listType(cloneWithRule(&integerType, "self == 'abc'")),
2086 }),
2087 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2088 },
2089 {name: "invalid rule under array items, parent has rule",
2090 obj: map[string]interface{}{
2091 "f": []interface{}{1},
2092 },
2093 schema: objectTypePtr(map[string]schema.Structural{
2094 "f": withRule(listType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
2095 }),
2096 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2097 },
2098 {name: "invalid rule under additionalProperties",
2099 obj: map[string]interface{}{
2100 "f": map[string]interface{}{"k": 1},
2101 },
2102 schema: objectTypePtr(map[string]schema.Structural{
2103 "f": mapType(cloneWithRule(&integerType, "self == 'abc'")),
2104 }),
2105 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2106 },
2107 {name: "invalid rule under additionalProperties, parent has rule",
2108 obj: map[string]interface{}{
2109 "f": map[string]interface{}{"k": 1},
2110 },
2111 schema: objectTypePtr(map[string]schema.Structural{
2112 "f": withRule(mapType(cloneWithRule(&integerType, "self == 'abc'")), "1 == 1"),
2113 }),
2114 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2115 },
2116 {name: "invalid rule under unescaped field name",
2117 obj: map[string]interface{}{
2118 "f": map[string]interface{}{
2119 "m": 1,
2120 },
2121 },
2122 schema: objectTypePtr(map[string]schema.Structural{
2123 "f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2124 }),
2125 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2126 },
2127 {name: "invalid rule under unescaped field name, parent has rule",
2128 obj: map[string]interface{}{
2129 "f": map[string]interface{}{
2130 "m": 1,
2131 },
2132 },
2133 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2134 "f": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2135 }), "1 == 1"),
2136 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2137 },
2138
2139 {name: "invalid rule under escaped field name",
2140 obj: map[string]interface{}{
2141 "f/2": map[string]interface{}{
2142 "m": 1,
2143 },
2144 },
2145 schema: objectTypePtr(map[string]schema.Structural{
2146 "f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2147 }),
2148 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2149 },
2150 {name: "invalid rule under escaped field name, parent has rule",
2151 obj: map[string]interface{}{
2152 "f/2": map[string]interface{}{
2153 "m": 1,
2154 },
2155 },
2156 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2157 "f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2158 }), "1 == 1"),
2159 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2160 },
2161 {name: "failing rule under escaped field name",
2162 obj: map[string]interface{}{
2163 "f/2": map[string]interface{}{
2164 "m": 1,
2165 },
2166 },
2167 schema: objectTypePtr(map[string]schema.Structural{
2168 "f/2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
2169 }),
2170 errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
2171 },
2172
2173 {name: "invalid rule under unescapable field name",
2174 obj: map[string]interface{}{
2175 "a@b": map[string]interface{}{
2176 "m": 1,
2177 },
2178 },
2179 schema: objectTypePtr(map[string]schema.Structural{
2180 "a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2181 }),
2182 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2183 },
2184 {name: "invalid rule under unescapable field name, parent has rule",
2185 obj: map[string]interface{}{
2186 "f@2": map[string]interface{}{
2187 "m": 1,
2188 },
2189 },
2190 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2191 "f@2": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 'abc'"),
2192 }), "1 == 1"),
2193 errors: []string{"found no matching overload for '_==_' applied to '(int, string)"},
2194 },
2195 {name: "failing rule under unescapable field name",
2196 obj: map[string]interface{}{
2197 "a@b": map[string]interface{}{
2198 "m": 1,
2199 },
2200 },
2201 schema: objectTypePtr(map[string]schema.Structural{
2202 "a@b": withRule(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2"),
2203 }),
2204 errors: []string{"Invalid value: \"object\": failed rule: self.m == 2"},
2205 },
2206 {name: "matchExpressions - 'values' must be specified when 'operator' is 'In' or 'NotIn'",
2207 obj: map[string]interface{}{
2208 "matchExpressions": []interface{}{
2209 map[string]interface{}{
2210 "key": "tier",
2211 "operator": "In",
2212 "values": []interface{}{},
2213 },
2214 },
2215 },
2216 schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, (rule.operator != "In" && rule.operator != "NotIn") || ((has(rule.values) && size(rule.values) > 0)))`),
2217 errors: []string{"failed rule"},
2218 },
2219 {name: "matchExpressions - 'values' may not be specified when 'operator' is 'Exists' or 'DoesNotExist'",
2220 obj: map[string]interface{}{
2221 "matchExpressions": []interface{}{
2222 map[string]interface{}{
2223 "key": "tier",
2224 "operator": "Exists",
2225 "values": []interface{}{"somevalue"},
2226 },
2227 },
2228 },
2229 schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, (rule.operator != "Exists" && rule.operator != "DoesNotExist") || ((!has(rule.values) || size(rule.values) == 0)))`),
2230 errors: []string{"failed rule"},
2231 },
2232 {name: "matchExpressions - invalid selector operator",
2233 obj: map[string]interface{}{
2234 "matchExpressions": []interface{}{
2235 map[string]interface{}{
2236 "key": "tier",
2237 "operator": "badop",
2238 "values": []interface{}{},
2239 },
2240 },
2241 },
2242 schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, rule.operator == "In" || rule.operator == "NotIn" || rule.operator == "DoesNotExist")`),
2243 errors: []string{"failed rule"},
2244 },
2245 {name: "matchExpressions - invalid label value",
2246 obj: map[string]interface{}{
2247 "matchExpressions": []interface{}{
2248 map[string]interface{}{
2249 "key": "badkey!",
2250 "operator": "Exists",
2251 "values": []interface{}{},
2252 },
2253 },
2254 },
2255 schema: genMatchSelectorSchema(`self.matchExpressions.all(rule, size(rule.key) <= 63 && rule.key.matches("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"))`),
2256 errors: []string{"failed rule"},
2257 },
2258 }
2259
2260 for _, tt := range tests {
2261 tt := tt
2262 t.Run(tt.name, func(t *testing.T) {
2263 t.Parallel()
2264 ctx := context.TODO()
2265 celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit)
2266 if celValidator == nil {
2267 t.Fatal("expected non nil validator")
2268 }
2269 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
2270 unmatched := map[string]struct{}{}
2271 for _, e := range tt.errors {
2272 unmatched[e] = struct{}{}
2273 }
2274 for _, err := range errs {
2275 if err.Type != field.ErrorTypeInvalid {
2276 t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
2277 continue
2278 }
2279 matched := false
2280 for expected := range unmatched {
2281 if strings.Contains(err.Error(), expected) {
2282 delete(unmatched, expected)
2283 matched = true
2284 break
2285 }
2286 }
2287 if !matched {
2288 t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
2289 }
2290 }
2291 if len(unmatched) > 0 {
2292 t.Errorf("expected errors %v", unmatched)
2293 }
2294 })
2295 }
2296 }
2297
2298 func genMatchSelectorSchema(rule string) *schema.Structural {
2299 s := withRule(objectType(map[string]schema.Structural{
2300 "matchExpressions": listType(objectTypePtr(map[string]schema.Structural{
2301 "key": stringType,
2302 "operator": stringType,
2303 "values": listType(&stringType),
2304 })),
2305 }), rule)
2306 return &s
2307 }
2308
2309 func TestCELValidationLimit(t *testing.T) {
2310 tests := []struct {
2311 name string
2312 schema *schema.Structural
2313 obj interface{}
2314 valid []string
2315 }{
2316 {
2317 name: "test limit",
2318 obj: objs(math.MaxInt64),
2319 schema: schemas(integerType),
2320 valid: []string{
2321 "self.val1 > 0",
2322 }},
2323 }
2324 for _, tt := range tests {
2325 t.Run(tt.name, func(t *testing.T) {
2326 ctx := context.TODO()
2327 for j := range tt.valid {
2328 validRule := tt.valid[j]
2329 t.Run(validRule, func(t *testing.T) {
2330 t.Parallel()
2331 s := withRule(*tt.schema, validRule)
2332 celValidator := validator(&s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit)
2333
2334
2335 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0)
2336 var found bool
2337 for _, err := range errs {
2338 if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
2339 found = true
2340 } else {
2341 t.Errorf("unexpected err: %v", err)
2342 }
2343 }
2344 if !found {
2345 t.Errorf("expect cost limit exceed err but did not find")
2346 }
2347 if len(errs) > 1 {
2348 t.Errorf("expect to only return cost budget exceed err once but got: %v", len(errs))
2349 }
2350
2351
2352 found = false
2353 celValidator = NewValidator(&s, true, 0)
2354 if celValidator == nil {
2355 t.Fatal("expected non nil validator")
2356 }
2357 errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
2358 for _, err := range errs {
2359 if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "no further validation rules will be run due to call cost exceeds limit for rule") {
2360 found = true
2361 break
2362 }
2363 }
2364 if !found {
2365 t.Errorf("expect PerCostLimit exceed err but did not find")
2366 }
2367 })
2368 }
2369 })
2370 }
2371
2372 }
2373
2374 func TestCELValidationContextCancellation(t *testing.T) {
2375 items := make([]interface{}, 1000)
2376 for i := int64(0); i < 1000; i++ {
2377 items[i] = i
2378 }
2379 tests := []struct {
2380 name string
2381 schema *schema.Structural
2382 obj map[string]interface{}
2383 rule string
2384 }{
2385 {name: "test cel validation with context cancellation",
2386 obj: map[string]interface{}{
2387 "array": items,
2388 },
2389 schema: objectTypePtr(map[string]schema.Structural{
2390 "array": listType(&integerType),
2391 }),
2392 rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
2393 },
2394 }
2395
2396 for _, tt := range tests {
2397 t.Run(tt.name, func(t *testing.T) {
2398 ctx := context.TODO()
2399 s := withRule(*tt.schema, tt.rule)
2400 celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
2401 if celValidator == nil {
2402 t.Fatal("expected non nil validator")
2403 }
2404 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
2405 for _, err := range errs {
2406 t.Errorf("unexpected error: %v", err)
2407 }
2408
2409
2410 found := false
2411 evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
2412 cancel()
2413 errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
2414 for _, err := range errs {
2415 if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") {
2416 found = true
2417 break
2418 }
2419 }
2420 if !found {
2421 t.Errorf("expect operation interrupted err but did not find")
2422 }
2423 })
2424 }
2425 }
2426
2427
2428
2429 const maxValidDepth = 250
2430
2431
2432 func TestCELMaxRecursionDepth(t *testing.T) {
2433 tests := []struct {
2434 name string
2435 schema *schema.Structural
2436 obj interface{}
2437 oldObj interface{}
2438 valid []string
2439 errors map[string]string
2440 costBudget int64
2441 isRoot bool
2442 expectSkipped bool
2443 }{
2444 {name: "test CEL maxRecursionDepth",
2445 obj: objs(true),
2446 schema: schemas(booleanType),
2447 valid: []string{
2448 strings.Repeat("self.val1"+" == ", maxValidDepth-1) + "self.val1",
2449 },
2450 errors: map[string]string{
2451 strings.Repeat("self.val1"+" == ", maxValidDepth) + "self.val1": "max recursion depth exceeded",
2452 },
2453 },
2454 }
2455
2456 for _, tt := range tests {
2457 t.Run(tt.name, func(t *testing.T) {
2458 tt.costBudget = celconfig.RuntimeCELCostBudget
2459 ctx := context.TODO()
2460 for j := range tt.valid {
2461 validRule := tt.valid[j]
2462 testName := validRule
2463 t.Run(testName, func(t *testing.T) {
2464 t.Parallel()
2465 s := withRule(*tt.schema, validRule)
2466 celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
2467 if celValidator == nil {
2468 t.Fatal("expected non nil validator")
2469 }
2470 errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
2471 for _, err := range errs {
2472 t.Errorf("unexpected error: %v", err)
2473 }
2474 if tt.expectSkipped {
2475
2476 if remainingBudget != tt.costBudget {
2477 t.Errorf("expected no cost expended for skipped validation, but got %d remaining from %d budget", remainingBudget, tt.costBudget)
2478 }
2479 return
2480 }
2481 })
2482 }
2483 for rule, expectErrToContain := range tt.errors {
2484 testName := rule
2485 if len(testName) > 127 {
2486 testName = testName[:127]
2487 }
2488 t.Run(testName, func(t *testing.T) {
2489 s := withRule(*tt.schema, rule)
2490 celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
2491 if celValidator == nil {
2492 t.Fatal("expected non nil validator")
2493 }
2494 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
2495 if len(errs) == 0 {
2496 t.Error("expected validation errors but got none")
2497 }
2498 for _, err := range errs {
2499 if err.Type != field.ErrorTypeInvalid || !strings.Contains(err.Error(), expectErrToContain) {
2500 t.Errorf("expected error to contain '%s', but got: %v", expectErrToContain, err)
2501 }
2502 }
2503 })
2504 }
2505 })
2506 }
2507 }
2508
2509 func TestMessageExpression(t *testing.T) {
2510 klog.LogToStderr(false)
2511 klog.InitFlags(nil)
2512 setDefaultVerbosity(2)
2513 defer klog.LogToStderr(true)
2514 tests := []struct {
2515 name string
2516 costBudget int64
2517 perCallLimit uint64
2518 message string
2519 messageExpression string
2520 expectedLogErr string
2521 expectedValidationErr string
2522 expectedRemainingBudget int64
2523 }{
2524 {
2525 name: "no cost error expected",
2526 messageExpression: `"static string"`,
2527 expectedValidationErr: "static string",
2528 costBudget: 300,
2529 expectedRemainingBudget: 300,
2530 },
2531 {
2532 name: "messageExpression takes precedence over message",
2533 message: "invisible",
2534 messageExpression: `"this is messageExpression"`,
2535 costBudget: celconfig.RuntimeCELCostBudget,
2536 expectedValidationErr: "this is messageExpression",
2537 },
2538 {
2539 name: "default rule message used if messageExpression does not eval to string",
2540 messageExpression: `true`,
2541 costBudget: celconfig.RuntimeCELCostBudget,
2542 expectedValidationErr: "failed rule",
2543 expectedRemainingBudget: celconfig.RuntimeCELCostBudget,
2544 },
2545 {
2546 name: "limit exceeded",
2547 messageExpression: `"string 1" + "string 2" + "string 3"`,
2548 costBudget: 1,
2549 expectedValidationErr: "messageExpression evaluation failed due to running out of cost budget",
2550 expectedRemainingBudget: -1,
2551 },
2552 {
2553 name: "messageExpression budget (str concat)",
2554 messageExpression: `"str1 " + self.str`,
2555 costBudget: 50,
2556 expectedValidationErr: "str1 a string",
2557 expectedRemainingBudget: 46,
2558 },
2559 {
2560 name: "runtime cost preserved if messageExpression fails during evaluation",
2561 message: "message not messageExpression",
2562 messageExpression: `"str1 " + ["a", "b", "c", "d"][4]`,
2563 costBudget: 50,
2564 expectedLogErr: "messageExpression evaluation failed due to: index out of bounds: 4",
2565 expectedValidationErr: "message not messageExpression",
2566 expectedRemainingBudget: 47,
2567 },
2568 {
2569 name: "runtime cost preserved if messageExpression fails during evaluation (no message set)",
2570 messageExpression: `"str1 " + ["a", "b", "c", "d"][4]`,
2571 costBudget: 50,
2572 expectedLogErr: "messageExpression evaluation failed due to: index out of bounds: 4",
2573 expectedValidationErr: "failed rule",
2574 expectedRemainingBudget: 47,
2575 },
2576 {
2577 name: "per-call limit exceeded during messageExpression execution",
2578 messageExpression: `"string 1" + "string 2" + "string 3"`,
2579 costBudget: celconfig.RuntimeCELCostBudget,
2580 perCallLimit: 1,
2581 expectedValidationErr: "call cost exceeds limit for messageExpression",
2582 expectedRemainingBudget: -1,
2583 },
2584 {
2585 name: "messageExpression is not allowed to generate a string with newlines",
2586 message: "message not messageExpression",
2587 messageExpression: `"str with \na newline"`,
2588 costBudget: celconfig.RuntimeCELCostBudget,
2589 expectedLogErr: "messageExpression should not contain line breaks",
2590 expectedValidationErr: "message not messageExpression",
2591 },
2592 {
2593 name: "messageExpression is not allowed to generate messages >5000 characters",
2594 message: "message not messageExpression",
2595 messageExpression: fmt.Sprintf(`"%s"`, genString(5121, 'a')),
2596 costBudget: celconfig.RuntimeCELCostBudget,
2597 expectedLogErr: "messageExpression beyond allowable length of 5120",
2598 expectedValidationErr: "message not messageExpression",
2599 },
2600 {
2601 name: "messageExpression is not allowed to generate an empty string",
2602 message: "message not messageExpression",
2603 messageExpression: `string("")`,
2604 costBudget: celconfig.RuntimeCELCostBudget,
2605 expectedLogErr: "messageExpression should evaluate to a non-empty string",
2606 expectedValidationErr: "message not messageExpression",
2607 },
2608 {
2609 name: "messageExpression is not allowed to generate a string with only spaces",
2610 message: "message not messageExpression",
2611 messageExpression: `string(" ")`,
2612 costBudget: celconfig.RuntimeCELCostBudget,
2613 expectedLogErr: "messageExpression should evaluate to a non-empty string",
2614 expectedValidationErr: "message not messageExpression",
2615 },
2616 }
2617 for _, tt := range tests {
2618 t.Run(tt.name, func(t *testing.T) {
2619 outputBuffer := strings.Builder{}
2620 klog.SetOutput(&outputBuffer)
2621
2622 ctx := context.TODO()
2623 var s schema.Structural
2624 if tt.message != "" {
2625 s = withRuleMessageAndMessageExpression(objectType(map[string]schema.Structural{
2626 "str": stringType}), "false", tt.message, tt.messageExpression)
2627 } else {
2628 s = withRuleAndMessageExpression(objectType(map[string]schema.Structural{
2629 "str": stringType}), "false", tt.messageExpression)
2630 }
2631 obj := map[string]interface{}{
2632 "str": "a string",
2633 }
2634
2635 callLimit := uint64(celconfig.PerCallLimit)
2636 if tt.perCallLimit != 0 {
2637 callLimit = tt.perCallLimit
2638 }
2639 celValidator := NewValidator(&s, false, callLimit)
2640 if celValidator == nil {
2641 t.Fatal("expected non nil validator")
2642 }
2643 errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, obj, nil, tt.costBudget)
2644 klog.Flush()
2645
2646 if len(errs) != 1 {
2647 t.Fatalf("expected 1 error, got %d", len(errs))
2648 }
2649
2650 if tt.expectedLogErr != "" {
2651 if !strings.Contains(outputBuffer.String(), tt.expectedLogErr) {
2652 t.Fatalf("did not find expected log error message: %q\n%q", tt.expectedLogErr, outputBuffer.String())
2653 }
2654 } else if tt.expectedLogErr == "" && outputBuffer.String() != "" {
2655 t.Fatalf("expected no log output, got: %q", outputBuffer.String())
2656 }
2657
2658 if tt.expectedValidationErr != "" {
2659 if !strings.Contains(errs[0].Error(), tt.expectedValidationErr) {
2660 t.Fatalf("did not find expected validation error message: %q", tt.expectedValidationErr)
2661 }
2662 }
2663
2664 if tt.expectedRemainingBudget != 0 {
2665 if tt.expectedRemainingBudget != remainingBudget {
2666 t.Fatalf("expected %d cost left, got %d", tt.expectedRemainingBudget, remainingBudget)
2667 }
2668 }
2669 })
2670 }
2671 }
2672
2673 func TestReasonAndFldPath(t *testing.T) {
2674 forbiddenReason := func() *apiextensions.FieldValueErrorReason {
2675 r := apiextensions.FieldValueForbidden
2676 return &r
2677 }()
2678 tests := []struct {
2679 name string
2680 schema *schema.Structural
2681 obj interface{}
2682 errors field.ErrorList
2683 }{
2684 {name: "Return error based on input reason",
2685 obj: map[string]interface{}{
2686 "f": map[string]interface{}{
2687 "m": 1,
2688 },
2689 },
2690 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2691 "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", forbiddenReason),
2692 }), "1 == 1"),
2693 errors: field.ErrorList{
2694 {
2695 Type: field.ErrorTypeForbidden,
2696 Field: "root.f",
2697 },
2698 },
2699 },
2700 {name: "Return error default is invalid",
2701 obj: map[string]interface{}{
2702 "f": map[string]interface{}{
2703 "m": 1,
2704 },
2705 },
2706 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2707 "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", "", nil),
2708 }), "1 == 1"),
2709 errors: field.ErrorList{
2710 {
2711 Type: field.ErrorTypeInvalid,
2712 Field: "root.f",
2713 },
2714 },
2715 },
2716 {name: "Return error based on input fieldPath",
2717 obj: map[string]interface{}{
2718 "f": map[string]interface{}{
2719 "m": 1,
2720 },
2721 },
2722 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
2723 "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ".m", forbiddenReason),
2724 }), "1 == 1"),
2725 errors: field.ErrorList{
2726 {
2727 Type: field.ErrorTypeForbidden,
2728 Field: "root.f.m",
2729 },
2730 },
2731 },
2732 {
2733 name: "multiple rules with custom reason and field path",
2734 obj: map[string]interface{}{
2735 "field1": "value1",
2736 "field2": "value2",
2737 "field3": "value3",
2738 },
2739 schema: &schema.Structural{
2740 Generic: schema.Generic{
2741 Type: "object",
2742 },
2743 Properties: map[string]schema.Structural{
2744 "field1": stringType,
2745 "field2": stringType,
2746 "field3": stringType,
2747 },
2748 Extensions: schema.Extensions{
2749 XValidations: apiextensions.ValidationRules{
2750 {
2751 Rule: `self.field2 != "value2"`,
2752 Reason: ptr.To(apiextensions.FieldValueDuplicate),
2753 FieldPath: ".field2",
2754 },
2755 {
2756 Rule: `self.field3 != "value3"`,
2757 Reason: ptr.To(apiextensions.FieldValueRequired),
2758 FieldPath: ".field3",
2759 },
2760 {
2761 Rule: `self.field1 != "value1"`,
2762 Reason: ptr.To(apiextensions.FieldValueForbidden),
2763 FieldPath: ".field1",
2764 },
2765 },
2766 },
2767 },
2768 errors: field.ErrorList{
2769 {
2770 Type: field.ErrorTypeDuplicate,
2771 Field: "root.field2",
2772 },
2773 {
2774 Type: field.ErrorTypeRequired,
2775 Field: "root.field3",
2776 },
2777 {
2778 Type: field.ErrorTypeForbidden,
2779 Field: "root.field1",
2780 },
2781 },
2782 },
2783 }
2784 for _, tt := range tests {
2785 t.Run(tt.name, func(t *testing.T) {
2786 ctx := context.TODO()
2787 celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit)
2788 if celValidator == nil {
2789 t.Fatal("expected non nil validator")
2790 }
2791 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, nil, celconfig.RuntimeCELCostBudget)
2792
2793 for i := range errs {
2794
2795 errs[i].Detail = ""
2796 errs[i].BadValue = nil
2797 }
2798
2799 require.ElementsMatch(t, tt.errors, errs)
2800 })
2801 }
2802 }
2803
2804 func TestValidateFieldPath(t *testing.T) {
2805 sts := schema.Structural{
2806 Generic: schema.Generic{
2807 Type: "object",
2808 },
2809 Properties: map[string]schema.Structural{
2810 "foo": {
2811 Generic: schema.Generic{
2812 Type: "object",
2813 AdditionalProperties: &schema.StructuralOrBool{
2814 Structural: &schema.Structural{
2815 Generic: schema.Generic{
2816 Type: "object",
2817 },
2818 Properties: map[string]schema.Structural{
2819 "subAdd": {
2820 Generic: schema.Generic{
2821 Type: "number",
2822 },
2823 },
2824 },
2825 },
2826 },
2827 },
2828 },
2829 "white space": {
2830 Generic: schema.Generic{
2831 Type: "number",
2832 },
2833 },
2834 "'foo'bar": {
2835 Generic: schema.Generic{
2836 Type: "number",
2837 },
2838 },
2839 "a": {
2840 Generic: schema.Generic{
2841 Type: "object",
2842 },
2843 Properties: map[string]schema.Structural{
2844 "foo's": {
2845 Generic: schema.Generic{
2846 Type: "number",
2847 },
2848 },
2849 "test\a": {
2850 Generic: schema.Generic{
2851 Type: "number",
2852 },
2853 },
2854 "bb[b": {
2855 Generic: schema.Generic{
2856 Type: "number",
2857 },
2858 },
2859 "bbb": {
2860 Generic: schema.Generic{
2861 Type: "object",
2862 },
2863 Properties: map[string]schema.Structural{
2864 "c": {
2865 Generic: schema.Generic{
2866 Type: "number",
2867 },
2868 },
2869 "34": {
2870 Generic: schema.Generic{
2871 Type: "number",
2872 },
2873 },
2874 },
2875 },
2876 "bbb.c": {
2877 Generic: schema.Generic{
2878 Type: "object",
2879 },
2880 Properties: map[string]schema.Structural{
2881 "a-b.3'4": {
2882 Generic: schema.Generic{
2883 Type: "number",
2884 },
2885 },
2886 },
2887 },
2888 },
2889 },
2890 "list": {
2891 Generic: schema.Generic{
2892 Type: "array",
2893 },
2894 Items: &schema.Structural{
2895 Generic: schema.Generic{
2896 Type: "object",
2897 },
2898 Properties: map[string]schema.Structural{
2899 "a": {
2900 Generic: schema.Generic{
2901 Type: "number",
2902 },
2903 },
2904 "a-b.3'4": {
2905 Generic: schema.Generic{
2906 Type: "number",
2907 },
2908 },
2909 },
2910 },
2911 },
2912 },
2913 }
2914
2915 path := field.NewPath("")
2916
2917 tests := []struct {
2918 name string
2919 fieldPath string
2920 pathOfFieldPath *field.Path
2921 schema *schema.Structural
2922 errDetail string
2923 validFieldPath *field.Path
2924 }{
2925 {
2926 name: "Valid .a",
2927 fieldPath: ".a",
2928 pathOfFieldPath: path,
2929 schema: &sts,
2930 validFieldPath: path.Child("a"),
2931 },
2932 {
2933 name: "Valid 'foo'bar",
2934 fieldPath: "['\\'foo\\'bar']",
2935 pathOfFieldPath: path,
2936 schema: &sts,
2937 validFieldPath: path.Child("'foo'bar"),
2938 },
2939 {
2940 name: "Invalid 'foo'bar",
2941 fieldPath: ".\\'foo\\'bar",
2942 pathOfFieldPath: path,
2943 schema: &sts,
2944 errDetail: "does not refer to a valid field",
2945 },
2946 {
2947 name: "Invalid with whitespace",
2948 fieldPath: ". a",
2949 pathOfFieldPath: path,
2950 schema: &sts,
2951 errDetail: "does not refer to a valid field",
2952 },
2953 {
2954 name: "Valid with whitespace inside field",
2955 fieldPath: ".white space",
2956 pathOfFieldPath: path,
2957 schema: &sts,
2958 },
2959 {
2960 name: "Valid with whitespace inside field",
2961 fieldPath: "['white space']",
2962 pathOfFieldPath: path,
2963 schema: &sts,
2964 },
2965 {
2966 name: "invalid dot annotation",
2967 fieldPath: ".a.bb[b",
2968 pathOfFieldPath: path,
2969 schema: &sts,
2970 errDetail: "does not refer to a valid field",
2971 },
2972 {
2973 name: "valid with .",
2974 fieldPath: ".a['bbb.c']",
2975 pathOfFieldPath: path,
2976 schema: &sts,
2977 validFieldPath: path.Child("a", "bbb.c"),
2978 },
2979 {
2980 name: "Unclosed ]",
2981 fieldPath: ".a['bbb.c'",
2982 pathOfFieldPath: path,
2983 schema: &sts,
2984 errDetail: "unexpected end of JSON path",
2985 },
2986 {
2987 name: "Unexpected end of JSON path",
2988 fieldPath: ".",
2989 pathOfFieldPath: path,
2990 schema: &sts,
2991 errDetail: "unexpected end of JSON path",
2992 },
2993 {
2994 name: "Valid map syntax .a.bbb",
2995 fieldPath: ".a['bbb.c']",
2996 pathOfFieldPath: path,
2997 schema: &sts,
2998 validFieldPath: path.Child("a").Child("bbb.c"),
2999 },
3000 {
3001 name: "Valid map key",
3002 fieldPath: ".foo.subAdd",
3003 pathOfFieldPath: path,
3004 schema: &sts,
3005 validFieldPath: path.Child("foo").Key("subAdd"),
3006 },
3007 {
3008 name: "Valid map key",
3009 fieldPath: ".foo['subAdd']",
3010 pathOfFieldPath: path,
3011 schema: &sts,
3012 validFieldPath: path.Child("foo").Key("subAdd"),
3013 },
3014 {
3015 name: "Invalid",
3016 fieldPath: ".a.foo's",
3017 pathOfFieldPath: path,
3018 schema: &sts,
3019 },
3020 {
3021 name: "Escaping",
3022 fieldPath: ".a['foo\\'s']",
3023 pathOfFieldPath: path,
3024 schema: &sts,
3025 validFieldPath: path.Child("a").Child("foo's"),
3026 },
3027 {
3028 name: "Escaping",
3029 fieldPath: ".a['test\\a']",
3030 pathOfFieldPath: path,
3031 schema: &sts,
3032 validFieldPath: path.Child("a").Child("test\a"),
3033 },
3034
3035 {
3036 name: "Invalid map key",
3037 fieldPath: ".a.foo",
3038 pathOfFieldPath: path,
3039 schema: &sts,
3040 errDetail: "does not refer to a valid field",
3041 },
3042 {
3043 name: "Malformed map key",
3044 fieldPath: ".a.bbb[0]",
3045 pathOfFieldPath: path,
3046 schema: &sts,
3047 errDetail: "expected single quoted string but got 0",
3048 },
3049 {
3050 name: "Valid refer for name has number",
3051 fieldPath: ".a.bbb.34",
3052 pathOfFieldPath: path,
3053 schema: &sts,
3054 },
3055 {
3056 name: "Map syntax for special field names",
3057 fieldPath: ".a.bbb['34']",
3058 pathOfFieldPath: path,
3059 schema: &sts,
3060
3061 },
3062 {
3063 name: "Valid .list",
3064 fieldPath: ".list",
3065 pathOfFieldPath: path,
3066 schema: &sts,
3067 validFieldPath: path.Child("list"),
3068 },
3069 {
3070 name: "Invalid list index",
3071 fieldPath: ".list[0]",
3072 pathOfFieldPath: path,
3073 schema: &sts,
3074 errDetail: "expected single quoted string but got 0",
3075 },
3076 {
3077 name: "Invalid list reference",
3078 fieldPath: ".list. a",
3079 pathOfFieldPath: path,
3080 schema: &sts,
3081 errDetail: "does not refer to a valid field",
3082 },
3083 {
3084 name: "Invalid .list.a",
3085 fieldPath: ".list['a']",
3086 pathOfFieldPath: path,
3087 schema: &sts,
3088 errDetail: "does not refer to a valid field",
3089 },
3090 {
3091 name: "Missing leading dot",
3092 fieldPath: "a",
3093 pathOfFieldPath: path,
3094 schema: &sts,
3095 errDetail: "expected [ or . but got: a",
3096 },
3097 {
3098 name: "Nonexistent field",
3099 fieldPath: ".c",
3100 pathOfFieldPath: path,
3101 schema: &sts,
3102 errDetail: "does not refer to a valid field",
3103 },
3104 {
3105 name: "Duplicate dots",
3106 fieldPath: ".a..b",
3107 pathOfFieldPath: path,
3108 schema: &sts,
3109 errDetail: "does not refer to a valid field",
3110 },
3111 {
3112 name: "object of array",
3113 fieldPath: ".list.a-b.34",
3114 pathOfFieldPath: path,
3115 schema: &sts,
3116 errDetail: "does not refer to a valid field",
3117 },
3118 }
3119
3120 for _, tc := range tests {
3121 t.Run(tc.name, func(t *testing.T) {
3122 validField, _, err := ValidFieldPath(tc.fieldPath, tc.schema)
3123
3124 if err == nil && tc.errDetail != "" {
3125 t.Errorf("expected err contains: %v but get nil", tc.errDetail)
3126 }
3127 if err != nil && tc.errDetail == "" {
3128 t.Errorf("unexpected error: %v", err)
3129 }
3130 if err != nil && !strings.Contains(err.Error(), tc.errDetail) {
3131 t.Errorf("expected error to contain: %v, but get: %v", tc.errDetail, err)
3132 }
3133 if tc.validFieldPath != nil && tc.validFieldPath.String() != path.Child(validField.String()).String() {
3134 t.Errorf("expected %v, got %v", tc.validFieldPath, path.Child(validField.String()))
3135 }
3136 })
3137 }
3138 }
3139
3140
3141
3142
3143
3144
3145
3146 func FixTabsOrDie(in string) string {
3147 lines := bytes.Split([]byte(in), []byte{'\n'})
3148 if len(lines[0]) == 0 && len(lines) > 1 {
3149 lines = lines[1:]
3150 }
3151
3152 var prefix []byte
3153 for _, c := range lines[0] {
3154 if c != '\t' {
3155 break
3156 }
3157 prefix = append(prefix, byte('\t'))
3158 }
3159
3160 for i := range lines {
3161 line := lines[i]
3162
3163 if i == len(lines)-1 && len(line) <= len(prefix) && bytes.TrimSpace(line) == nil {
3164 lines[i] = []byte{}
3165 break
3166 }
3167 if !bytes.HasPrefix(line, prefix) {
3168 minRange := i - 5
3169 maxRange := i + 5
3170 if minRange < 0 {
3171 minRange = 0
3172 }
3173 if maxRange > len(lines) {
3174 maxRange = len(lines)
3175 }
3176 panic(fmt.Errorf("line %d doesn't start with expected number (%d) of tabs (%v-%v):\n%v", i, len(prefix), minRange, maxRange, string(bytes.Join(lines[minRange:maxRange], []byte{'\n'}))))
3177 }
3178 lines[i] = line[len(prefix):]
3179 }
3180
3181 joined := string(bytes.Join(lines, []byte{'\n'}))
3182
3183
3184
3185 return strings.ReplaceAll(joined, "\t", " ")
3186 }
3187
3188
3189 func mustSchema(source string) *schema.Structural {
3190 source = FixTabsOrDie(source)
3191 d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
3192 props := &apiextensions.JSONSchemaProps{}
3193 if err := d.Decode(props); err != nil {
3194 panic(err)
3195 }
3196 convertedProps := &apiextensionsinternal.JSONSchemaProps{}
3197 if err := apiextensions.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(props, convertedProps, nil); err != nil {
3198 panic(err)
3199 }
3200
3201 res, err := schema.NewStructural(convertedProps)
3202 if err != nil {
3203 panic(err)
3204 }
3205 return res
3206 }
3207
3208
3209 func mustUnstructured(source string) interface{} {
3210 source = FixTabsOrDie(source)
3211 d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096)
3212 var res interface{}
3213 if err := d.Decode(&res); err != nil {
3214 panic(err)
3215 }
3216 return res
3217 }
3218
3219 type warningRecorder struct {
3220 mu sync.Mutex
3221 warnings []string
3222 }
3223
3224
3225 func (r *warningRecorder) AddWarning(agent, text string) {
3226 r.mu.Lock()
3227 defer r.mu.Unlock()
3228 r.warnings = append(r.warnings, text)
3229 }
3230
3231 func (r *warningRecorder) Warnings() []string {
3232 r.mu.Lock()
3233 defer r.mu.Unlock()
3234
3235 warnings := make([]string, len(r.warnings))
3236 copy(warnings, r.warnings)
3237 return warnings
3238 }
3239
3240 func TestRatcheting(t *testing.T) {
3241 cases := []struct {
3242 name string
3243 schema *schema.Structural
3244 oldObj interface{}
3245 newObj interface{}
3246
3247
3248
3249 errors []string
3250
3251
3252
3253
3254 warnings []string
3255
3256 runtimeCostBudget int64
3257 }{
3258 {
3259 name: "normal CEL expression",
3260 schema: mustSchema(`
3261 type: object
3262 properties:
3263 foo:
3264 type: string
3265 x-kubernetes-validations:
3266 - rule: self == "bar"
3267 message: "gotta be baz"
3268 `),
3269 oldObj: mustUnstructured(`
3270 foo: baz
3271 `),
3272 newObj: mustUnstructured(`
3273 foo: baz
3274 `),
3275 warnings: []string{
3276 `root.foo: Invalid value: "string": gotta be baz`,
3277 },
3278 },
3279 {
3280 name: "normal CEL expression thats a descendent of an atomic array whose parent is totally unchanged",
3281 schema: mustSchema(`
3282 type: array
3283 x-kubernetes-list-type: atomic
3284 items:
3285 type: object
3286 properties:
3287 bar:
3288 type: string
3289 x-kubernetes-validations:
3290 - rule: self == "baz"
3291 message: "gotta be baz"
3292 `),
3293
3294
3295
3296 oldObj: mustUnstructured(`
3297 - bar: bar
3298 `),
3299 newObj: mustUnstructured(`
3300 - bar: bar
3301 `),
3302 warnings: []string{
3303 `root[0].bar: Invalid value: "string": gotta be baz`,
3304 },
3305 },
3306 {
3307 name: "normal CEL expression thats a descendent of a set whose parent is totally unchanged",
3308 schema: mustSchema(`
3309 type: array
3310 x-kubernetes-list-type: set
3311 items:
3312 type: number
3313 x-kubernetes-validations:
3314 - rule: int(self) % 2 == 1
3315 message: "gotta be odd"
3316 `),
3317
3318
3319
3320 oldObj: mustUnstructured(`
3321 - 1
3322 - 2
3323 `),
3324 newObj: mustUnstructured(`
3325 - 1
3326 - 2
3327 `),
3328 warnings: []string{
3329 `root[1]: Invalid value: "number": gotta be odd`,
3330 },
3331 },
3332 {
3333 name: "normal CEL expression thats a descendent of a set and one of its siblings has changed",
3334 schema: mustSchema(`
3335 type: object
3336 properties:
3337 stringField:
3338 type: string
3339 setArray:
3340 type: array
3341 x-kubernetes-list-type: set
3342 items:
3343 type: number
3344 x-kubernetes-validations:
3345 - rule: int(self) % 2 == 1
3346 message: "gotta be odd"
3347 `),
3348 oldObj: mustUnstructured(`
3349 stringField: foo
3350 setArray:
3351 - 1
3352 - 3
3353 - 2
3354 `),
3355 newObj: mustUnstructured(`
3356 stringField: changed but ratcheted
3357 setArray:
3358 - 1
3359 - 3
3360 - 2
3361 `),
3362 warnings: []string{
3363 `root.setArray[2]: Invalid value: "number": gotta be odd`,
3364 },
3365 },
3366 {
3367 name: "descendent of a map list whose parent is unchanged",
3368 schema: mustSchema(`
3369 type: array
3370 x-kubernetes-list-type: map
3371 x-kubernetes-list-map-keys: ["key"]
3372 items:
3373 type: object
3374 properties:
3375 key:
3376 type: string
3377 value:
3378 type: string
3379 x-kubernetes-validations:
3380 - rule: self == "baz"
3381 message: "gotta be baz"
3382 `),
3383 oldObj: mustUnstructured(`
3384 - key: foo
3385 value: notbaz
3386 - key: bar
3387 value: notbaz
3388 `),
3389 newObj: mustUnstructured(`
3390 - key: foo
3391 value: notbaz
3392 - key: bar
3393 value: notbaz
3394 - key: baz
3395 value: baz
3396 `),
3397 warnings: []string{
3398 `root[0].value: Invalid value: "string": gotta be baz`,
3399 `root[1].value: Invalid value: "string": gotta be baz`,
3400 },
3401 },
3402 {
3403 name: "descendent of a map list whose siblings have changed",
3404 schema: mustSchema(`
3405 type: array
3406 x-kubernetes-list-type: map
3407 x-kubernetes-list-map-keys: ["key"]
3408 items:
3409 type: object
3410 properties:
3411 key:
3412 type: string
3413 value:
3414 type: string
3415 x-kubernetes-validations:
3416 - rule: self == "baz"
3417 message: "gotta be baz"
3418 `),
3419 oldObj: mustUnstructured(`
3420 - key: foo
3421 value: notbaz
3422 - key: bar
3423 value: notbaz
3424 `),
3425 newObj: mustUnstructured(`
3426 - key: foo
3427 value: baz
3428 - key: bar
3429 value: notbaz
3430 `),
3431 warnings: []string{
3432 `root[1].value: Invalid value: "string": gotta be baz`,
3433 },
3434 },
3435 {
3436 name: "descendent of a map whose parent is totally unchanged",
3437 schema: mustSchema(`
3438 type: object
3439 properties:
3440 stringField:
3441 type: string
3442 mapField:
3443 type: object
3444 properties:
3445 foo:
3446 type: string
3447 x-kubernetes-validations:
3448 - rule: self == "baz"
3449 message: "gotta be baz"
3450 mapField:
3451 type: object
3452 properties:
3453 bar:
3454 type: string
3455 x-kubernetes-validations:
3456 - rule: self == "baz"
3457 message: "gotta be nested baz"
3458 `),
3459 oldObj: mustUnstructured(`
3460 stringField: foo
3461 mapField:
3462 foo: notbaz
3463 mapField:
3464 bar: notbaz
3465 `),
3466 newObj: mustUnstructured(`
3467 stringField: foo
3468 mapField:
3469 foo: notbaz
3470 mapField:
3471 bar: notbaz
3472 `),
3473 warnings: []string{
3474 `root.mapField.foo: Invalid value: "string": gotta be baz`,
3475 `root.mapField.mapField.bar: Invalid value: "string": gotta be nested baz`,
3476 },
3477 },
3478 {
3479 name: "descendent of a map whose siblings have changed",
3480 schema: mustSchema(`
3481 type: object
3482 properties:
3483 stringField:
3484 type: string
3485 mapField:
3486 type: object
3487 properties:
3488 foo:
3489 type: string
3490 x-kubernetes-validations:
3491 - rule: self == "baz"
3492 message: "gotta be baz"
3493 mapField:
3494 type: object
3495 properties:
3496 bar:
3497 type: string
3498 x-kubernetes-validations:
3499 - rule: self == "baz"
3500 message: "gotta be baz"
3501 otherBar:
3502 type: string
3503 x-kubernetes-validations:
3504 - rule: self == "otherBaz"
3505 message: "gotta be otherBaz"
3506 `),
3507 oldObj: mustUnstructured(`
3508 stringField: foo
3509 mapField:
3510 foo: baz
3511 mapField:
3512 bar: notbaz
3513 otherBar: nototherBaz
3514 `),
3515 newObj: mustUnstructured(`
3516 stringField: foo
3517 mapField:
3518 foo: notbaz
3519 mapField:
3520 bar: notbaz
3521 otherBar: otherBaz
3522 `),
3523 errors: []string{
3524
3525 `root.mapField.foo: Invalid value: "string": gotta be baz`,
3526 },
3527 warnings: []string{
3528
3529 `root.mapField.mapField.bar: Invalid value: "string": gotta be baz`,
3530 },
3531 },
3532 {
3533 name: "normal CEL expression thats a descendent of an atomic array whose siblings has changed",
3534 schema: mustSchema(`
3535 type: object
3536 properties:
3537 stringField:
3538 type: string
3539 atomicArray:
3540 type: array
3541 x-kubernetes-list-type: atomic
3542 items:
3543 type: object
3544 properties:
3545 bar:
3546 type: string
3547 x-kubernetes-validations:
3548 - rule: self == "baz"
3549 message: "gotta be baz"
3550 `),
3551 oldObj: mustUnstructured(`
3552 stringField: foo
3553 atomicArray:
3554 - bar: bar
3555 `),
3556 newObj: mustUnstructured(`
3557 stringField: changed but ratcheted
3558 atomicArray:
3559 - bar: bar
3560 `),
3561 warnings: []string{
3562 `root.atomicArray[0].bar: Invalid value: "string": gotta be baz`,
3563 },
3564 },
3565 {
3566 name: "we can't ratchet a normal CEL expression from an uncorrelatable part of the schema whose parent nodes has changed",
3567 schema: mustSchema(`
3568 type: array
3569 x-kubernetes-list-type: atomic
3570 items:
3571 type: object
3572 properties:
3573 bar:
3574 type: string
3575 x-kubernetes-validations:
3576 - rule: self == "baz"
3577 message: "gotta be baz"
3578 `),
3579
3580
3581
3582 oldObj: mustUnstructured(`
3583 - bar: bar
3584 `),
3585 newObj: mustUnstructured(`
3586 - bar: bar
3587 - bar: baz
3588 `),
3589 errors: []string{
3590 `root[0].bar: Invalid value: "string": gotta be baz`,
3591 },
3592 },
3593 {
3594 name: "transition rules never ratchet for correlatable schemas",
3595 schema: mustSchema(`
3596 type: object
3597 properties:
3598 foo:
3599 type: string
3600 x-kubernetes-validations:
3601 - rule: oldSelf != "bar" && self == "baz"
3602 message: gotta be baz
3603 `),
3604 oldObj: mustUnstructured(`
3605 foo: bar
3606 `),
3607 newObj: mustUnstructured(`
3608 foo: bar
3609 `),
3610 errors: []string{
3611 `root.foo: Invalid value: "string": gotta be baz`,
3612 },
3613 },
3614 {
3615 name: "changing field path does not change ratcheting logic",
3616 schema: mustSchema(`
3617 type: object
3618 x-kubernetes-validations:
3619 - rule: self.foo == "baz"
3620 message: gotta be baz
3621 fieldPath: ".foo"
3622 properties:
3623 bar:
3624 type: string
3625 foo:
3626 type: string
3627 `),
3628 oldObj: mustUnstructured(`
3629 foo: bar
3630 `),
3631
3632
3633 newObj: mustUnstructured(`
3634 foo: bar
3635 bar: invalid
3636 `),
3637 errors: []string{
3638 `root.foo: Invalid value: "object": gotta be baz`,
3639 },
3640 },
3641 {
3642 name: "cost budget errors are not ratcheted",
3643 schema: mustSchema(`
3644 type: string
3645 minLength: 5
3646 x-kubernetes-validations:
3647 - rule: self == "baz"
3648 message: gotta be baz
3649 `),
3650 oldObj: "unchanged",
3651 newObj: "unchanged",
3652 runtimeCostBudget: 1,
3653 errors: []string{
3654 `validation failed due to running out of cost budget, no further validation rules will be run`,
3655 },
3656 },
3657 {
3658 name: "compile errors are not ratcheted",
3659 schema: mustSchema(`
3660 type: string
3661 x-kubernetes-validations:
3662 - rule: asdausidyhASDNJm
3663 message: gotta be baz
3664 `),
3665 oldObj: "unchanged",
3666 newObj: "unchanged",
3667 errors: []string{
3668 `rule compile error: compilation failed: ERROR: <input>:1:1: undeclared reference to 'asdausidyhASDNJm'`,
3669 },
3670 },
3671 {
3672 name: "typemeta fields are not ratcheted",
3673 schema: mustSchema(`
3674 type: object
3675 properties:
3676 apiVersion:
3677 type: string
3678 x-kubernetes-validations:
3679 - rule: self == "v1"
3680 kind:
3681 type: string
3682 x-kubernetes-validations:
3683 - rule: self == "Pod"
3684 `),
3685 oldObj: mustUnstructured(`
3686 apiVersion: v2
3687 kind: Baz
3688 `),
3689 newObj: mustUnstructured(`
3690 apiVersion: v2
3691 kind: Baz
3692 `),
3693 errors: []string{
3694 `root.apiVersion: Invalid value: "string": failed rule: self == "v1"`,
3695 `root.kind: Invalid value: "string": failed rule: self == "Pod"`,
3696 },
3697 },
3698 {
3699 name: "nested typemeta fields may still be ratcheted",
3700 schema: mustSchema(`
3701 type: object
3702 properties:
3703 list:
3704 type: array
3705 x-kubernetes-list-type: map
3706 x-kubernetes-list-map-keys: ["name"]
3707 maxItems: 2
3708 items:
3709 type: object
3710 properties:
3711 name:
3712 type: string
3713 apiVersion:
3714 type: string
3715 x-kubernetes-validations:
3716 - rule: self == "v1"
3717 kind:
3718 type: string
3719 x-kubernetes-validations:
3720 - rule: self == "Pod"
3721 subField:
3722 type: object
3723 properties:
3724 apiVersion:
3725 type: string
3726 x-kubernetes-validations:
3727 - rule: self == "v1"
3728 kind:
3729 type: string
3730 x-kubernetes-validations:
3731 - rule: self == "Pod"
3732 otherField:
3733 type: string
3734 `),
3735 oldObj: mustUnstructured(`
3736 subField:
3737 apiVersion: v2
3738 kind: Baz
3739 list:
3740 - name: entry1
3741 apiVersion: v2
3742 kind: Baz
3743 - name: entry2
3744 apiVersion: v3
3745 kind: Bar
3746 `),
3747 newObj: mustUnstructured(`
3748 subField:
3749 apiVersion: v2
3750 kind: Baz
3751 otherField: newValue
3752 list:
3753 - name: entry1
3754 apiVersion: v2
3755 kind: Baz
3756 otherField: newValue2
3757 - name: entry2
3758 apiVersion: v3
3759 kind: Bar
3760 otherField: newValue3
3761 `),
3762 warnings: []string{
3763 `root.subField.apiVersion: Invalid value: "string": failed rule: self == "v1"`,
3764 `root.subField.kind: Invalid value: "string": failed rule: self == "Pod"`,
3765 `root.list[0].apiVersion: Invalid value: "string": failed rule: self == "v1"`,
3766 `root.list[0].kind: Invalid value: "string": failed rule: self == "Pod"`,
3767 `root.list[1].apiVersion: Invalid value: "string": failed rule: self == "v1"`,
3768 `root.list[1].kind: Invalid value: "string": failed rule: self == "Pod"`,
3769 },
3770 },
3771 }
3772
3773 for _, c := range cases {
3774 t.Run(c.name, func(t *testing.T) {
3775 validator := NewValidator(c.schema, false, celconfig.PerCallLimit)
3776 require.NotNil(t, validator)
3777 recorder := &warningRecorder{}
3778 ctx := warning.WithWarningRecorder(context.TODO(), recorder)
3779 budget := c.runtimeCostBudget
3780 if budget == 0 {
3781 budget = celconfig.RuntimeCELCostBudget
3782 }
3783 errs, _ := validator.Validate(
3784 ctx,
3785 field.NewPath("root"),
3786 c.schema,
3787 c.newObj,
3788 c.oldObj,
3789 budget,
3790 WithRatcheting(common.NewCorrelatedObject(c.newObj, c.oldObj, &model.Structural{Structural: c.schema})),
3791 )
3792
3793 require.Len(t, errs, len(c.errors), "must have expected number of errors")
3794 require.Len(t, recorder.Warnings(), len(c.warnings), "must have expected number of warnings")
3795
3796
3797 for _, expectedErr := range c.errors {
3798 found := false
3799 for _, err := range errs {
3800 if strings.Contains(err.Error(), expectedErr) {
3801 found = true
3802 break
3803 }
3804 }
3805
3806 assert.True(t, found, "expected error %q not found", expectedErr)
3807 }
3808
3809
3810 for _, expectedWarning := range c.warnings {
3811 found := false
3812 for _, warning := range recorder.Warnings() {
3813 if warning == expectedWarning {
3814 found = true
3815 break
3816 }
3817 }
3818 assert.True(t, found, "expected warning %q not found", expectedWarning)
3819 }
3820
3821 })
3822 }
3823 }
3824
3825
3826 func TestOptionalOldSelf(t *testing.T) {
3827 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
3828
3829 tests := []struct {
3830 name string
3831 schema *schema.Structural
3832 obj interface{}
3833 oldObj interface{}
3834 errors []string
3835 }{
3836 {
3837 name: "allow new value if old value is null",
3838 obj: map[string]interface{}{
3839 "foo": "bar",
3840 },
3841 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
3842 "foo": stringType,
3843 }), "self.foo == 'not bar' || !oldSelf.hasValue()"),
3844 },
3845 {
3846 name: "block new value if old value is not null",
3847 obj: map[string]interface{}{
3848 "foo": "invalid",
3849 },
3850 oldObj: map[string]interface{}{
3851 "foo": "bar",
3852 },
3853 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
3854 "foo": stringType,
3855 }), "self.foo == 'valid' || !oldSelf.hasValue()"),
3856 errors: []string{"failed rule"},
3857 },
3858 {
3859 name: "allow invalid new value if old value is also invalid",
3860 obj: map[string]interface{}{
3861 "foo": "invalid again",
3862 },
3863 oldObj: map[string]interface{}{
3864 "foo": "invalid",
3865 },
3866 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
3867 "foo": stringType,
3868 }), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
3869 },
3870 {
3871 name: "allow invalid new value if old value is also invalid with chained optionals",
3872 obj: map[string]interface{}{
3873 "foo": "invalid again",
3874 },
3875 oldObj: map[string]interface{}{
3876 "foo": "invalid",
3877 },
3878 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
3879 "foo": stringType,
3880 }), "self.foo == 'valid' || oldSelf.foo.orValue('') != 'valid'"),
3881 },
3882 {
3883 name: "block invalid new value if old value is valid",
3884 obj: map[string]interface{}{
3885 "foo": "invalid",
3886 },
3887 oldObj: map[string]interface{}{
3888 "foo": "valid",
3889 },
3890 schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
3891 "foo": stringType,
3892 }), "self.foo == 'valid' || (oldSelf.hasValue() && oldSelf.value().foo != 'valid')"),
3893 errors: []string{"failed rule"},
3894 },
3895 {
3896 name: "create: new min or allow higher than oldValue",
3897 obj: 10,
3898 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3899 },
3900 {
3901 name: "block create: new min or allow higher than oldValue",
3902 obj: 9,
3903
3904
3905 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3906 errors: []string{"failed rule"},
3907 },
3908 {
3909 name: "update: new min or allow higher than oldValue",
3910 obj: 10,
3911 oldObj: 5,
3912 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3913 },
3914 {
3915 name: "ratchet update: new min or allow higher than oldValue",
3916 obj: 9,
3917 oldObj: 5,
3918 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3919 },
3920 {
3921 name: "ratchet noop update: new min or allow higher than oldValue",
3922 obj: 5,
3923 oldObj: 5,
3924 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3925 },
3926 {
3927 name: "block update: new min or allow higher than oldValue",
3928 obj: 4,
3929 oldObj: 5,
3930 schema: cloneWithRule(&integerType, "self >= 10 || (oldSelf.hasValue() && oldSelf.value() <= self)"),
3931 errors: []string{"failed rule"},
3932 },
3933 }
3934
3935 for _, tt := range tests {
3936 tt := tt
3937 tp := true
3938 for i := range tt.schema.XValidations {
3939 tt.schema.XValidations[i].OptionalOldSelf = &tp
3940 }
3941
3942 t.Run(tt.name, func(t *testing.T) {
3943
3944
3945 ctx := context.TODO()
3946 celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, false), celconfig.PerCallLimit)
3947 if celValidator == nil {
3948 t.Fatal("expected non nil validator")
3949 }
3950 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), tt.schema, tt.obj, tt.oldObj, math.MaxInt)
3951 unmatched := map[string]struct{}{}
3952 for _, e := range tt.errors {
3953 unmatched[e] = struct{}{}
3954 }
3955 for _, err := range errs {
3956 if err.Type != field.ErrorTypeInvalid {
3957 t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
3958 continue
3959 }
3960 matched := false
3961 for expected := range unmatched {
3962 if strings.Contains(err.Error(), expected) {
3963 delete(unmatched, expected)
3964 matched = true
3965 break
3966 }
3967 }
3968 if !matched {
3969 t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
3970 }
3971 }
3972 if len(unmatched) > 0 {
3973 t.Errorf("expected errors %v", unmatched)
3974 }
3975 })
3976 }
3977 }
3978
3979
3980
3981 func TestOptionalOldSelfCheckForNull(t *testing.T) {
3982 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
3983
3984 tests := []struct {
3985 name string
3986 schema schema.Structural
3987 obj interface{}
3988 oldObj interface{}
3989 }{
3990 {
3991 name: "object",
3992 obj: map[string]interface{}{
3993 "foo": "bar",
3994 },
3995 oldObj: map[string]interface{}{
3996 "foo": "baz",
3997 },
3998 schema: withRule(objectType(map[string]schema.Structural{
3999 "foo": stringType,
4000 }), `!oldSelf.hasValue() || self.foo == "bar"`),
4001 },
4002 {
4003 name: "object - conditional field",
4004 obj: map[string]interface{}{
4005 "foo": "bar",
4006 },
4007 oldObj: map[string]interface{}{
4008 "foo": "baz",
4009 },
4010 schema: withRule(objectType(map[string]schema.Structural{
4011 "foo": stringType,
4012 }), `self.foo != "bar" || oldSelf.?foo.orValue("baz") == "baz"`),
4013 },
4014 {
4015 name: "string",
4016 obj: "bar",
4017 oldObj: "baz",
4018 schema: withRule(stringType, `
4019 !oldSelf.hasValue() || self == "bar"
4020 `),
4021 },
4022 {
4023 name: "integer",
4024 obj: 1,
4025 oldObj: 2,
4026 schema: withRule(integerType, `
4027 !oldSelf.hasValue() || self == 1
4028 `),
4029 },
4030 {
4031 name: "number",
4032 obj: 1.1,
4033 oldObj: 2.2,
4034 schema: withRule(numberType, `
4035 !oldSelf.hasValue() || self == 1.1
4036 `),
4037 },
4038 {
4039 name: "boolean",
4040 obj: true,
4041 oldObj: false,
4042 schema: withRule(booleanType, `
4043 !oldSelf.hasValue() || self == true
4044 `),
4045 },
4046 {
4047 name: "array",
4048 obj: []interface{}{"bar"},
4049 oldObj: []interface{}{"baz"},
4050 schema: withRule(arrayType("", nil, &stringSchema), `
4051 !oldSelf.hasValue() || self[0] == "bar"
4052 `),
4053 },
4054 {
4055 name: "array - conditional index",
4056 obj: []interface{}{},
4057 oldObj: []interface{}{
4058 "baz",
4059 },
4060 schema: withRule(arrayType("", nil, &stringSchema), `
4061 self.size() > 0 || oldSelf[?0].orValue("baz") == "baz"
4062 `),
4063 },
4064 {
4065 name: "set-array",
4066 obj: []interface{}{"bar"},
4067 oldObj: []interface{}{"baz"},
4068 schema: withRule(arrayType("set", nil, &stringSchema), `
4069 !oldSelf.hasValue() || self[0] == "bar"
4070 `),
4071 },
4072 {
4073 name: "map-array",
4074 obj: []interface{}{map[string]interface{}{
4075 "key": "foo",
4076 "value": "bar",
4077 }},
4078 oldObj: []interface{}{map[string]interface{}{
4079 "key": "foo",
4080 "value": "baz",
4081 }},
4082 schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
4083 "key": stringType,
4084 "value": stringType,
4085 })), `
4086 !oldSelf.hasValue() || self[0].value == "bar"
4087 `),
4088 },
4089 }
4090
4091 for _, tt := range tests {
4092 tt := tt
4093 tp := true
4094 for i := range tt.schema.XValidations {
4095 tt.schema.XValidations[i].OptionalOldSelf = &tp
4096 }
4097
4098 t.Run(tt.name, func(t *testing.T) {
4099 ctx := context.TODO()
4100 celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
4101 if celValidator == nil {
4102 t.Fatal("expected non nil validator")
4103 }
4104
4105 t.Run("null old", func(t *testing.T) {
4106 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, nil, math.MaxInt)
4107 if len(errs) != 0 {
4108 t.Errorf("expected no errors, but got: %v", errs)
4109 }
4110 })
4111
4112 t.Run("non-null old", func(t *testing.T) {
4113 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.oldObj, math.MaxInt)
4114 if len(errs) != 0 {
4115 t.Errorf("expected no errors, but got: %v", errs)
4116 }
4117 })
4118 })
4119 }
4120 }
4121
4122
4123 func TestOptionalOldSelfIsOptionalType(t *testing.T) {
4124 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CRDValidationRatcheting, true)()
4125
4126 cases := []struct {
4127 name string
4128 schema schema.Structural
4129 obj interface{}
4130 errors []string
4131 }{
4132 {
4133 name: "forbid direct usage of optional integer",
4134 schema: withRule(integerType, `
4135 oldSelf + self > 5
4136 `),
4137 obj: 5,
4138 errors: []string{"no matching overload for '_+_' applied to '(optional(int), int)"},
4139 },
4140 {
4141 name: "forbid direct usage of optional string",
4142 schema: withRule(stringType, `
4143 oldSelf == "foo"
4144 `),
4145 obj: "bar",
4146 errors: []string{"no matching overload for '_==_' applied to '(optional(string), string)"},
4147 },
4148 {
4149 name: "forbid direct usage of optional array",
4150 schema: withRule(arrayType("", nil, &stringSchema), `
4151 oldSelf.all(x, x == x)
4152 `),
4153 obj: []interface{}{"bar"},
4154 errors: []string{"expression of type 'optional(list(string))' cannot be range of a comprehension"},
4155 },
4156 {
4157 name: "forbid direct usage of optional array element",
4158 schema: withRule(arrayType("", nil, &stringSchema), `
4159 oldSelf[0] == "foo"
4160 `),
4161 obj: []interface{}{"bar"},
4162 errors: []string{"found no matching overload for '_==_' applied to '(optional(string), string)"},
4163 },
4164 {
4165 name: "forbid direct usage of optional struct",
4166 schema: withRule(arrayType("map", []string{"key"}, objectTypePtr(map[string]schema.Structural{
4167 "key": stringType,
4168 "value": stringType,
4169 })), `oldSelf.key == "foo"`),
4170 obj: []interface{}{map[string]interface{}{
4171 "key": "bar",
4172 "value": "baz",
4173 }},
4174 errors: []string{"does not support field selection"},
4175 },
4176 }
4177
4178 for _, tt := range cases {
4179 t.Run(tt.name, func(t *testing.T) {
4180 ctx := context.TODO()
4181
4182 for i := range tt.schema.XValidations {
4183 tt.schema.XValidations[i].OptionalOldSelf = ptr.To(true)
4184 }
4185
4186 celValidator := validator(&tt.schema, false, model.SchemaDeclType(&tt.schema, false), celconfig.PerCallLimit)
4187 if celValidator == nil {
4188 t.Fatal("expected non nil validator")
4189 }
4190 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &tt.schema, tt.obj, tt.obj, math.MaxInt)
4191 unmatched := map[string]struct{}{}
4192 for _, e := range tt.errors {
4193 unmatched[e] = struct{}{}
4194 }
4195 for _, err := range errs {
4196 if err.Type != field.ErrorTypeInvalid {
4197 t.Errorf("expected only ErrorTypeInvalid errors, but got: %v", err)
4198 continue
4199 }
4200 matched := false
4201 for expected := range unmatched {
4202 if strings.Contains(err.Error(), expected) {
4203 delete(unmatched, expected)
4204 matched = true
4205 break
4206 }
4207 }
4208 if !matched {
4209 t.Errorf("expected error to contain one of %v, but got: %v", unmatched, err)
4210 }
4211 }
4212
4213 if len(unmatched) > 0 {
4214 t.Errorf("expected errors %v", unmatched)
4215 }
4216 })
4217 }
4218 }
4219
4220 func genString(n int, c rune) string {
4221 b := strings.Builder{}
4222 for i := 0; i < n; i++ {
4223 _, err := b.WriteRune(c)
4224 if err != nil {
4225 panic(err)
4226 }
4227 }
4228 return b.String()
4229 }
4230
4231 func setDefaultVerbosity(v int) {
4232 f := flag.CommandLine.Lookup("v")
4233 _ = f.Value.Set(fmt.Sprintf("%d", v))
4234 }
4235
4236 func BenchmarkCELValidationWithContext(b *testing.B) {
4237 items := make([]interface{}, 1000)
4238 for i := int64(0); i < 1000; i++ {
4239 items[i] = i
4240 }
4241 tests := []struct {
4242 name string
4243 schema *schema.Structural
4244 obj map[string]interface{}
4245 rule string
4246 }{
4247 {name: "benchmark for cel validation with context",
4248 obj: map[string]interface{}{
4249 "array": items,
4250 },
4251 schema: objectTypePtr(map[string]schema.Structural{
4252 "array": listType(&integerType),
4253 }),
4254 rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
4255 },
4256 }
4257
4258 for _, tt := range tests {
4259 b.Run(tt.name, func(b *testing.B) {
4260 ctx := context.TODO()
4261 s := withRule(*tt.schema, tt.rule)
4262 celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
4263 if celValidator == nil {
4264 b.Fatal("expected non nil validator")
4265 }
4266 for i := 0; i < b.N; i++ {
4267 errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
4268 for _, err := range errs {
4269 b.Fatalf("validation failed: %v", err)
4270 }
4271 }
4272 })
4273 }
4274 }
4275
4276 func BenchmarkCELValidationWithCancelledContext(b *testing.B) {
4277 items := make([]interface{}, 1000)
4278 for i := int64(0); i < 1000; i++ {
4279 items[i] = i
4280 }
4281 tests := []struct {
4282 name string
4283 schema *schema.Structural
4284 obj map[string]interface{}
4285 rule string
4286 }{
4287 {name: "benchmark for cel validation with context",
4288 obj: map[string]interface{}{
4289 "array": items,
4290 },
4291 schema: objectTypePtr(map[string]schema.Structural{
4292 "array": listType(&integerType),
4293 }),
4294 rule: "self.array.map(e, e * 20).filter(e, e > 50).exists(e, e == 60)",
4295 },
4296 }
4297
4298 for _, tt := range tests {
4299 b.Run(tt.name, func(b *testing.B) {
4300 ctx := context.TODO()
4301 s := withRule(*tt.schema, tt.rule)
4302 celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
4303 if celValidator == nil {
4304 b.Fatal("expected non nil validator")
4305 }
4306 for i := 0; i < b.N; i++ {
4307 evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
4308 cancel()
4309 errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
4310
4311
4312
4313
4314
4315
4316
4317 if len(errs) == 0 {
4318 b.Errorf("expect operation interrupted err but did not find")
4319 }
4320 }
4321 })
4322 }
4323 }
4324
4325
4326
4327 func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) {
4328 for _, rule := range []string{
4329 "self.getMonth() >= 0",
4330 "oldSelf.getMonth() >= 0",
4331 } {
4332 b.Run(rule, func(b *testing.B) {
4333 obj := map[string]interface{}{
4334 "datetime": time.Time{}.Format(strfmt.ISO8601LocalTime),
4335 }
4336 s := &schema.Structural{
4337 Generic: schema.Generic{
4338 Type: "object",
4339 },
4340 Properties: map[string]schema.Structural{
4341 "datetime": {
4342 Generic: schema.Generic{
4343 Type: "string",
4344 },
4345 ValueValidation: &schema.ValueValidation{
4346 Format: "date-time",
4347 },
4348 Extensions: schema.Extensions{
4349 XValidations: []apiextensions.ValidationRule{
4350 {Rule: rule},
4351 },
4352 },
4353 },
4354 },
4355 }
4356 validator := NewValidator(s, true, celconfig.PerCallLimit)
4357 if validator == nil {
4358 b.Fatal("expected non nil validator")
4359 }
4360
4361 ctx := context.TODO()
4362 root := field.NewPath("root")
4363
4364 b.ReportAllocs()
4365 b.ResetTimer()
4366 for i := 0; i < b.N; i++ {
4367 errs, _ := validator.Validate(ctx, root, s, obj, obj, celconfig.RuntimeCELCostBudget)
4368 for _, err := range errs {
4369 b.Errorf("unexpected error: %v", err)
4370 }
4371 }
4372 })
4373 }
4374 }
4375
4376 func primitiveType(typ, format string) schema.Structural {
4377 result := schema.Structural{
4378 Generic: schema.Generic{
4379 Type: typ,
4380 },
4381 }
4382 if len(format) != 0 {
4383 result.ValueValidation = &schema.ValueValidation{
4384 Format: format,
4385 }
4386 }
4387 return result
4388 }
4389
4390 var (
4391 integerType = primitiveType("integer", "")
4392 int32Type = primitiveType("integer", "int32")
4393 int64Type = primitiveType("integer", "int64")
4394 numberType = primitiveType("number", "")
4395 floatType = primitiveType("number", "float")
4396 doubleType = primitiveType("number", "double")
4397 stringType = primitiveType("string", "")
4398 byteType = primitiveType("string", "byte")
4399 booleanType = primitiveType("boolean", "")
4400
4401 durationFormat = primitiveType("string", "duration")
4402 dateFormat = primitiveType("string", "date")
4403 dateTimeFormat = primitiveType("string", "date-time")
4404 )
4405
4406 func listType(items *schema.Structural) schema.Structural {
4407 return arrayType("atomic", nil, items)
4408 }
4409
4410 func listTypePtr(items *schema.Structural) *schema.Structural {
4411 l := listType(items)
4412 return &l
4413 }
4414
4415 func listSetType(items *schema.Structural) schema.Structural {
4416 return arrayType("set", nil, items)
4417 }
4418
4419 func listMapType(keys []string, items *schema.Structural) schema.Structural {
4420 return arrayType("map", keys, items)
4421 }
4422
4423 func listMapTypePtr(keys []string, items *schema.Structural) *schema.Structural {
4424 l := listMapType(keys, items)
4425 return &l
4426 }
4427
4428 func arrayType(listType string, keys []string, items *schema.Structural) schema.Structural {
4429 result := schema.Structural{
4430 Generic: schema.Generic{
4431 Type: "array",
4432 },
4433 Extensions: schema.Extensions{
4434 XListType: &listType,
4435 },
4436 Items: items,
4437 }
4438 if len(keys) > 0 && listType == "map" {
4439 result.Extensions.XListMapKeys = keys
4440 }
4441 return result
4442 }
4443
4444 func ValsEqualThemselvesAndDataLiteral(val1, val2 string, dataLiteral string) string {
4445 return fmt.Sprintf("%s == %s && %s == %s && %s == %s", val1, dataLiteral, dataLiteral, val1, val1, val2)
4446 }
4447
4448 func objs(val ...interface{}) map[string]interface{} {
4449 result := make(map[string]interface{}, len(val))
4450 for i, v := range val {
4451 result[fmt.Sprintf("val%d", i+1)] = v
4452 }
4453 return result
4454 }
4455
4456 func schemas(valSchema ...schema.Structural) *schema.Structural {
4457 result := make(map[string]schema.Structural, len(valSchema))
4458 for i, v := range valSchema {
4459 result[fmt.Sprintf("val%d", i+1)] = v
4460 }
4461 return objectTypePtr(result)
4462 }
4463
4464 func objectType(props map[string]schema.Structural) schema.Structural {
4465 return schema.Structural{
4466 Generic: schema.Generic{
4467 Type: "object",
4468 },
4469 Properties: props,
4470 }
4471 }
4472
4473 func objectTypePtr(props map[string]schema.Structural) *schema.Structural {
4474 o := objectType(props)
4475 return &o
4476 }
4477
4478 func mapType(valSchema *schema.Structural) schema.Structural {
4479 result := schema.Structural{
4480 Generic: schema.Generic{
4481 Type: "object",
4482 AdditionalProperties: &schema.StructuralOrBool{Bool: true, Structural: valSchema},
4483 },
4484 }
4485 return result
4486 }
4487
4488 func mapTypePtr(valSchema *schema.Structural) *schema.Structural {
4489 m := mapType(valSchema)
4490 return &m
4491 }
4492
4493 func intOrStringType() schema.Structural {
4494 return schema.Structural{
4495 Extensions: schema.Extensions{
4496 XIntOrString: true,
4497 },
4498 }
4499 }
4500
4501 func withRule(s schema.Structural, rule string) schema.Structural {
4502 s.Extensions.XValidations = apiextensions.ValidationRules{
4503 {
4504 Rule: rule,
4505 },
4506 }
4507 return s
4508 }
4509
4510 func withRuleMessageAndMessageExpression(s schema.Structural, rule, message, messageExpression string) schema.Structural {
4511 s.Extensions.XValidations = apiextensions.ValidationRules{
4512 {
4513 Rule: rule,
4514 Message: message,
4515 MessageExpression: messageExpression,
4516 },
4517 }
4518 return s
4519 }
4520
4521 func withReasonAndFldPath(s schema.Structural, rule, jsonPath string, reason *apiextensions.FieldValueErrorReason) schema.Structural {
4522 s.Extensions.XValidations = apiextensions.ValidationRules{
4523 {
4524 Rule: rule,
4525 FieldPath: jsonPath,
4526 Reason: reason,
4527 },
4528 }
4529 return s
4530 }
4531
4532 func withRuleAndMessageExpression(s schema.Structural, rule, messageExpression string) schema.Structural {
4533 s.Extensions.XValidations = apiextensions.ValidationRules{
4534 {
4535 Rule: rule,
4536 MessageExpression: messageExpression,
4537 },
4538 }
4539 return s
4540 }
4541
4542 func withRulePtr(s *schema.Structural, rule string) *schema.Structural {
4543 s.Extensions.XValidations = apiextensions.ValidationRules{
4544 {
4545 Rule: rule,
4546 },
4547 }
4548 return s
4549 }
4550
4551 func cloneWithRule(s *schema.Structural, rule string) *schema.Structural {
4552 s = s.DeepCopy()
4553 return withRulePtr(s, rule)
4554 }
4555
4556 func withMaxLength(s schema.Structural, maxLength *int64) schema.Structural {
4557 if s.ValueValidation == nil {
4558 s.ValueValidation = &schema.ValueValidation{}
4559 }
4560 s.ValueValidation.MaxLength = maxLength
4561 return s
4562 }
4563
4564 func withMaxItems(s schema.Structural, maxItems *int64) schema.Structural {
4565 if s.ValueValidation == nil {
4566 s.ValueValidation = &schema.ValueValidation{}
4567 }
4568 s.ValueValidation.MaxItems = maxItems
4569 return s
4570 }
4571
4572 func withMaxProperties(s schema.Structural, maxProperties *int64) schema.Structural {
4573 if s.ValueValidation == nil {
4574 s.ValueValidation = &schema.ValueValidation{}
4575 }
4576 s.ValueValidation.MaxProperties = maxProperties
4577 return s
4578 }
4579
4580 func withDefault(dflt interface{}, s schema.Structural) schema.Structural {
4581 s.Generic.Default = schema.JSON{Object: dflt}
4582 return s
4583 }
4584
4585 func withNullable(nullable bool, s schema.Structural) schema.Structural {
4586 s.Generic.Nullable = nullable
4587 return s
4588 }
4589
4590 func withNullablePtr(nullable bool, s schema.Structural) *schema.Structural {
4591 s.Generic.Nullable = nullable
4592 return &s
4593 }
4594
4595 func nilInterfaceOfStringSlice() []interface{} {
4596 var slice []interface{} = nil
4597 return slice
4598 }
4599
View as plain text