k8s.io/apiserver@v0.31.1/pkg/cel/common/equality_test.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package common_test 18 19 import ( 20 "errors" 21 "fmt" 22 "reflect" 23 "strings" 24 "testing" 25 26 "k8s.io/apimachinery/pkg/util/yaml" 27 "k8s.io/apiserver/pkg/cel/common" 28 "k8s.io/apiserver/pkg/cel/openapi" 29 "k8s.io/kube-openapi/pkg/validation/spec" 30 ) 31 32 type TestCase struct { 33 Name string 34 35 // Expected old value after traversal. If nil, then the traversal should fail. 36 OldValue interface{} 37 38 // Expected value after traversal. If nil, then the traversal should fail. 39 NewValue interface{} 40 41 // Whether OldValue and NewValue are considered to be equal. 42 // Defaults to reflect.DeepEqual comparison of the two. Can be overridden to 43 // true here if the two values are not DeepEqual, but are considered equal 44 // for instance due to map-list reordering. 45 ExpectEqual bool 46 47 // Schema to provide to the correlated object 48 Schema common.Schema 49 50 // Array of field names and indexes to traverse to get to the value 51 KeyPath []interface{} 52 53 // Root object to traverse from 54 RootObject interface{} 55 RootOldObject interface{} 56 } 57 58 func (c TestCase) Run() error { 59 // Create the correlated object 60 correlatedObject := common.NewCorrelatedObject(c.RootObject, c.RootOldObject, c.Schema) 61 62 // Traverse the correlated object 63 var err error 64 for _, key := range c.KeyPath { 65 if correlatedObject == nil { 66 break 67 } 68 69 switch k := key.(type) { 70 case string: 71 correlatedObject = correlatedObject.Key(k) 72 case int: 73 correlatedObject = correlatedObject.Index(k) 74 default: 75 return errors.New("key must be a string or int") 76 } 77 if err != nil { 78 return err 79 } 80 } 81 82 if correlatedObject == nil { 83 if c.OldValue != nil || c.NewValue != nil { 84 return fmt.Errorf("expected non-nil value, got nil") 85 } 86 } else { 87 // Check that the correlated object has the expected values 88 if !reflect.DeepEqual(correlatedObject.Value, c.NewValue) { 89 return fmt.Errorf("expected value %v, got %v", c.NewValue, correlatedObject.Value) 90 } 91 if !reflect.DeepEqual(correlatedObject.OldValue, c.OldValue) { 92 return fmt.Errorf("expected old value %v, got %v", c.OldValue, correlatedObject.OldValue) 93 } 94 95 // Check that the correlated object is considered equal to the expected value 96 if (c.ExpectEqual || reflect.DeepEqual(correlatedObject.Value, correlatedObject.OldValue)) != correlatedObject.CachedDeepEqual() { 97 return fmt.Errorf("expected equal, got not equal") 98 } 99 } 100 101 return nil 102 } 103 104 // Creates a *spec.Schema Schema by decoding the given YAML. Panics on error 105 func mustSchema(source string) *openapi.Schema { 106 d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096) 107 res := &spec.Schema{} 108 if err := d.Decode(res); err != nil { 109 panic(err) 110 } 111 return &openapi.Schema{Schema: res} 112 } 113 114 // Creates an *unstructured by decoding the given YAML. Panics on error 115 func mustUnstructured(source string) interface{} { 116 d := yaml.NewYAMLOrJSONDecoder(strings.NewReader(source), 4096) 117 var res interface{} 118 if err := d.Decode(&res); err != nil { 119 panic(err) 120 } 121 return res 122 } 123 124 func TestCorrelation(t *testing.T) { 125 // Tests ensure that the output of following keypath using the given 126 // schema and root objects yields the provided new value and old value. 127 // If new or old are nil, then ensures that the traversal failed due to 128 // uncorrelatable field path. 129 // Also confirms that CachedDeepEqual output is equal to expected result of 130 // reflect.DeepEqual of the new and old values. 131 cases := []TestCase{ 132 { 133 Name: "Basic Key", 134 RootObject: mustUnstructured(`a: b`), 135 RootOldObject: mustUnstructured(`a: b`), 136 Schema: mustSchema(` 137 properties: 138 a: { type: string } 139 `), 140 KeyPath: []interface{}{"a"}, 141 NewValue: "b", 142 OldValue: "b", 143 }, 144 { 145 Name: "Atomic Array not correlatable", 146 RootObject: mustUnstructured(`[a, b]`), 147 RootOldObject: mustUnstructured(`[a, b]`), 148 Schema: mustSchema(` 149 items: 150 type: string 151 `), 152 KeyPath: []interface{}{1}, 153 }, 154 { 155 Name: "Added Key Not In Old Object", 156 RootObject: mustUnstructured(` 157 a: b 158 c: d 159 `), 160 RootOldObject: mustUnstructured(` 161 a: b 162 `), 163 Schema: mustSchema(` 164 properties: 165 a: { type: string } 166 c: { type: string } 167 `), 168 KeyPath: []interface{}{"c"}, 169 }, 170 { 171 Name: "Added Index Not In Old Object", 172 RootObject: mustUnstructured(` 173 - a 174 - b 175 - c 176 `), 177 RootOldObject: mustUnstructured(` 178 - a 179 - b 180 `), 181 Schema: mustSchema(` 182 items: 183 type: string 184 `), 185 KeyPath: []interface{}{2}, 186 }, 187 { 188 Name: "Changed Index In Old Object not correlatable", 189 RootObject: []interface{}{ 190 "a", 191 "b", 192 }, 193 RootOldObject: []interface{}{ 194 "a", 195 "oldB", 196 }, 197 Schema: mustSchema(` 198 items: 199 type: string 200 `), 201 KeyPath: []interface{}{1}, 202 }, 203 { 204 Name: "Changed Index In Nested Old Object", 205 RootObject: []interface{}{ 206 "a", 207 "b", 208 }, 209 RootOldObject: []interface{}{ 210 "a", 211 "oldB", 212 }, 213 Schema: mustSchema(` 214 items: 215 type: string 216 `), 217 KeyPath: []interface{}{}, 218 NewValue: []interface{}{"a", "b"}, 219 OldValue: []interface{}{"a", "oldB"}, 220 }, 221 { 222 Name: "Changed Key In Old Object", 223 RootObject: map[string]interface{}{ 224 "a": "b", 225 }, 226 RootOldObject: map[string]interface{}{ 227 "a": "oldB", 228 }, 229 Schema: mustSchema(` 230 properties: 231 a: { type: string } 232 `), 233 KeyPath: []interface{}{"a"}, 234 NewValue: "b", 235 OldValue: "oldB", 236 }, 237 { 238 Name: "Replaced Key In Old Object", 239 RootObject: map[string]interface{}{ 240 "a": "b", 241 }, 242 RootOldObject: map[string]interface{}{ 243 "b": "a", 244 }, 245 Schema: mustSchema(` 246 properties: 247 a: { type: string } 248 `), 249 KeyPath: []interface{}{}, 250 NewValue: map[string]interface{}{"a": "b"}, 251 OldValue: map[string]interface{}{"b": "a"}, 252 }, 253 { 254 Name: "Added Key In Old Object", 255 RootObject: map[string]interface{}{ 256 "a": "b", 257 }, 258 RootOldObject: map[string]interface{}{}, 259 Schema: mustSchema(` 260 properties: 261 a: { type: string } 262 `), 263 KeyPath: []interface{}{}, 264 NewValue: map[string]interface{}{"a": "b"}, 265 OldValue: map[string]interface{}{}, 266 }, 267 { 268 Name: "Changed list to map", 269 RootObject: map[string]interface{}{ 270 "a": "b", 271 }, 272 RootOldObject: []interface{}{"a", "b"}, 273 Schema: mustSchema(` 274 properties: 275 a: { type: string } 276 `), 277 KeyPath: []interface{}{}, 278 NewValue: map[string]interface{}{"a": "b"}, 279 OldValue: []interface{}{"a", "b"}, 280 }, 281 { 282 Name: "Changed string to map", 283 RootObject: map[string]interface{}{ 284 "a": "b", 285 }, 286 RootOldObject: "a string", 287 Schema: mustSchema(` 288 properties: 289 a: { type: string } 290 `), 291 KeyPath: []interface{}{}, 292 NewValue: map[string]interface{}{"a": "b"}, 293 OldValue: "a string", 294 }, 295 { 296 Name: "Map list type", 297 RootObject: mustUnstructured(` 298 foo: 299 - bar: baz 300 val: newBazValue 301 `), 302 RootOldObject: mustUnstructured(` 303 foo: 304 - bar: fizz 305 val: fizzValue 306 - bar: baz 307 val: bazValue 308 `), 309 Schema: mustSchema(` 310 properties: 311 foo: 312 type: array 313 items: 314 type: object 315 properties: 316 bar: 317 type: string 318 val: 319 type: string 320 x-kubernetes-list-type: map 321 x-kubernetes-list-map-keys: 322 - bar 323 `), 324 KeyPath: []interface{}{"foo", 0, "val"}, 325 NewValue: "newBazValue", 326 OldValue: "bazValue", 327 }, 328 { 329 Name: "Atomic list item should not correlate", 330 RootObject: mustUnstructured(` 331 foo: 332 - bar: baz 333 val: newValue 334 `), 335 RootOldObject: mustUnstructured(` 336 foo: 337 - bar: fizz 338 val: fizzValue 339 - bar: baz 340 val: barValue 341 `), 342 Schema: mustSchema(` 343 properties: 344 foo: 345 type: array 346 items: 347 type: object 348 properties: 349 bar: 350 type: string 351 val: 352 type: string 353 x-kubernetes-list-type: atomic 354 `), 355 KeyPath: []interface{}{"foo", 0, "val"}, 356 }, 357 { 358 Name: "Map used inside of map list type should correlate", 359 RootObject: mustUnstructured(` 360 foo: 361 - key: keyValue 362 bar: 363 baz: newValue 364 `), 365 RootOldObject: mustUnstructured(` 366 foo: 367 - key: otherKeyValue 368 bar: 369 baz: otherOldValue 370 - key: altKeyValue 371 bar: 372 baz: altOldValue 373 - key: keyValue 374 bar: 375 baz: oldValue 376 `), 377 Schema: mustSchema(` 378 properties: 379 foo: 380 type: array 381 items: 382 type: object 383 properties: 384 key: 385 type: string 386 bar: 387 type: object 388 properties: 389 baz: 390 type: string 391 x-kubernetes-list-type: map 392 x-kubernetes-list-map-keys: 393 - key 394 `), 395 KeyPath: []interface{}{"foo", 0, "bar", "baz"}, 396 NewValue: "newValue", 397 OldValue: "oldValue", 398 }, 399 { 400 Name: "Map used inside another map should correlate", 401 RootObject: mustUnstructured(` 402 foo: 403 key: keyValue 404 bar: 405 baz: newValue 406 `), 407 RootOldObject: mustUnstructured(` 408 foo: 409 key: otherKeyValue 410 bar: 411 baz: otherOldValue 412 altFoo: 413 key: altKeyValue 414 bar: 415 baz: altOldValue 416 otherFoo: 417 key: keyValue 418 bar: 419 baz: oldValue 420 `), 421 Schema: mustSchema(` 422 properties: 423 foo: 424 type: object 425 properties: 426 key: 427 type: string 428 bar: 429 type: object 430 properties: 431 baz: 432 type: string 433 `), 434 KeyPath: []interface{}{"foo", "bar"}, 435 NewValue: map[string]interface{}{"baz": "newValue"}, 436 OldValue: map[string]interface{}{"baz": "otherOldValue"}, 437 }, 438 { 439 Name: "Nested map equal to old", 440 RootObject: mustUnstructured(` 441 foo: 442 key: newKeyValue 443 bar: 444 baz: value 445 `), 446 RootOldObject: mustUnstructured(` 447 foo: 448 key: keyValue 449 bar: 450 baz: value 451 `), 452 Schema: mustSchema(` 453 properties: 454 foo: 455 type: object 456 properties: 457 key: 458 type: string 459 bar: 460 type: object 461 properties: 462 baz: 463 type: string 464 `), 465 KeyPath: []interface{}{"foo", "bar"}, 466 NewValue: map[string]interface{}{"baz": "value"}, 467 OldValue: map[string]interface{}{"baz": "value"}, 468 }, 469 { 470 Name: "Re-ordered list considered equal to old value due to map keys", 471 RootObject: mustUnstructured(` 472 foo: 473 - key: keyValue 474 bar: 475 baz: value 476 - key: altKeyValue 477 bar: 478 baz: altValue 479 `), 480 RootOldObject: mustUnstructured(` 481 foo: 482 - key: altKeyValue 483 bar: 484 baz: altValue 485 - key: keyValue 486 bar: 487 baz: value 488 `), 489 Schema: mustSchema(` 490 properties: 491 foo: 492 type: array 493 items: 494 type: object 495 properties: 496 key: 497 type: string 498 bar: 499 type: object 500 properties: 501 baz: 502 type: string 503 x-kubernetes-list-type: map 504 x-kubernetes-list-map-keys: 505 - key 506 `), 507 KeyPath: []interface{}{"foo"}, 508 NewValue: mustUnstructured(` 509 - key: keyValue 510 bar: 511 baz: value 512 - key: altKeyValue 513 bar: 514 baz: altValue 515 `), 516 OldValue: mustUnstructured(` 517 - key: altKeyValue 518 bar: 519 baz: altValue 520 - key: keyValue 521 bar: 522 baz: value 523 `), 524 ExpectEqual: true, 525 }, 526 { 527 Name: "Correlate unknown string key via additional properties", 528 RootObject: mustUnstructured(` 529 foo: 530 key: keyValue 531 bar: 532 baz: newValue 533 `), 534 RootOldObject: mustUnstructured(` 535 foo: 536 key: otherKeyValue 537 bar: 538 baz: otherOldValue 539 `), 540 Schema: mustSchema(` 541 properties: 542 foo: 543 type: object 544 additionalProperties: 545 properties: 546 baz: 547 type: string 548 `), 549 KeyPath: []interface{}{"foo", "bar", "baz"}, 550 NewValue: "newValue", 551 OldValue: "otherOldValue", 552 }, 553 { 554 Name: "Changed map value", 555 RootObject: mustUnstructured(` 556 foo: 557 key: keyValue 558 bar: 559 baz: newValue 560 `), 561 RootOldObject: mustUnstructured(` 562 foo: 563 key: keyValue 564 bar: 565 baz: oldValue 566 `), 567 Schema: mustSchema(` 568 properties: 569 foo: 570 type: object 571 properties: 572 key: 573 type: string 574 bar: 575 type: object 576 properties: 577 baz: 578 type: string 579 `), 580 KeyPath: []interface{}{"foo", "bar"}, 581 NewValue: mustUnstructured(` 582 baz: newValue 583 `), 584 OldValue: mustUnstructured(` 585 baz: oldValue 586 `), 587 }, 588 { 589 Name: "Changed nested map value", 590 RootObject: mustUnstructured(` 591 foo: 592 key: keyValue 593 bar: 594 baz: newValue 595 `), 596 RootOldObject: mustUnstructured(` 597 foo: 598 key: keyValue 599 bar: 600 baz: oldValue 601 `), 602 Schema: mustSchema(` 603 properties: 604 foo: 605 type: object 606 properties: 607 key: 608 type: string 609 bar: 610 type: object 611 properties: 612 baz: 613 type: string 614 `), 615 KeyPath: []interface{}{"foo"}, 616 NewValue: mustUnstructured(` 617 key: keyValue 618 bar: 619 baz: newValue 620 `), 621 OldValue: mustUnstructured(` 622 key: keyValue 623 bar: 624 baz: oldValue 625 `), 626 }, 627 { 628 Name: "unchanged list type set with atomic map values", 629 Schema: mustSchema(` 630 properties: 631 foo: 632 type: array 633 items: 634 type: object 635 x-kubernetes-map-type: atomic 636 properties: 637 key: 638 type: string 639 bar: 640 type: string 641 x-kubernetes-list-type: set 642 `), 643 RootObject: mustUnstructured(` 644 foo: 645 - key: key1 646 bar: value1 647 - key: key2 648 bar: value2 649 `), 650 RootOldObject: mustUnstructured(` 651 foo: 652 - key: key1 653 bar: value1 654 - key: key2 655 bar: value2 656 `), 657 KeyPath: []interface{}{"foo"}, 658 NewValue: mustUnstructured(` 659 - key: key1 660 bar: value1 661 - key: key2 662 bar: value2 663 `), 664 OldValue: mustUnstructured(` 665 - key: key1 666 bar: value1 667 - key: key2 668 bar: value2 669 `), 670 }, 671 { 672 Name: "changed list type set with atomic map values", 673 Schema: mustSchema(` 674 properties: 675 foo: 676 type: array 677 items: 678 type: object 679 x-kubernetes-map-type: atomic 680 properties: 681 key: 682 type: string 683 bar: 684 type: string 685 x-kubernetes-list-type: set 686 `), 687 RootObject: mustUnstructured(` 688 foo: 689 - key: key1 690 bar: value1 691 - key: key2 692 bar: newValue2 693 `), 694 RootOldObject: mustUnstructured(` 695 foo: 696 - key: key1 697 bar: value1 698 - key: key2 699 bar: value2 700 `), 701 KeyPath: []interface{}{"foo"}, 702 NewValue: mustUnstructured(` 703 - key: key1 704 bar: value1 705 - key: key2 706 bar: newValue2 707 `), 708 OldValue: mustUnstructured(` 709 - key: key1 710 bar: value1 711 - key: key2 712 bar: value2 713 `), 714 }, 715 { 716 Name: "elements of list type set with atomic map values are not correlated", 717 Schema: mustSchema(` 718 properties: 719 foo: 720 type: array 721 items: 722 type: object 723 x-kubernetes-map-type: atomic 724 properties: 725 key: 726 type: string 727 bar: 728 type: string 729 x-kubernetes-list-type: set 730 `), 731 RootObject: mustUnstructured(` 732 foo: 733 - key: key1 734 bar: value1 735 - key: key2 736 bar: newValue2 737 `), 738 RootOldObject: mustUnstructured(` 739 foo: 740 - key: key1 741 bar: value1 742 - key: key2 743 bar: value2 744 `), 745 KeyPath: []interface{}{"foo", 0, "key"}, 746 NewValue: nil, 747 }, 748 } 749 for _, c := range cases { 750 t.Run(c.Name, func(t *testing.T) { 751 if err := c.Run(); err != nil { 752 t.Errorf("unexpected error: %v", err) 753 } 754 }) 755 } 756 }