sigs.k8s.io/controller-runtime@v0.18.2/pkg/envtest/komega/equalobject_test.go (about) 1 package komega 2 3 import ( 4 "testing" 5 6 . "github.com/onsi/gomega" 7 appsv1 "k8s.io/api/apps/v1" 8 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 "sigs.k8s.io/controller-runtime/pkg/client" 11 ) 12 13 func TestEqualObjectMatcher(t *testing.T) { 14 cases := []struct { 15 name string 16 original client.Object 17 modified client.Object 18 options []EqualObjectOption 19 want bool 20 }{ 21 { 22 name: "succeed with equal objects", 23 original: &appsv1.Deployment{ 24 ObjectMeta: metav1.ObjectMeta{ 25 Name: "test", 26 }, 27 }, 28 modified: &appsv1.Deployment{ 29 ObjectMeta: metav1.ObjectMeta{ 30 Name: "test", 31 }, 32 }, 33 want: true, 34 }, 35 { 36 name: "fail with non equal objects", 37 original: &appsv1.Deployment{ 38 ObjectMeta: metav1.ObjectMeta{ 39 Name: "test", 40 }, 41 }, 42 modified: &appsv1.Deployment{ 43 ObjectMeta: metav1.ObjectMeta{ 44 Name: "somethingelse", 45 }, 46 }, 47 want: false, 48 }, 49 { 50 name: "succeeds if ignored fields do not match", 51 original: &appsv1.Deployment{ 52 ObjectMeta: metav1.ObjectMeta{ 53 Name: "test", 54 Labels: map[string]string{"somelabel": "somevalue"}, 55 OwnerReferences: []metav1.OwnerReference{{ 56 Name: "controller", 57 }}, 58 }, 59 }, 60 modified: &appsv1.Deployment{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Name: "somethingelse", 63 Labels: map[string]string{"somelabel": "anothervalue"}, 64 OwnerReferences: []metav1.OwnerReference{{ 65 Name: "another", 66 }}, 67 }, 68 }, 69 want: true, 70 options: []EqualObjectOption{ 71 IgnorePaths{ 72 "ObjectMeta.Name", 73 "ObjectMeta.CreationTimestamp", 74 "ObjectMeta.Labels.somelabel", 75 "ObjectMeta.OwnerReferences[0].Name", 76 "Spec.Template.ObjectMeta", 77 }, 78 }, 79 }, 80 { 81 name: "succeeds if ignored fields in json notation do not match", 82 original: &appsv1.Deployment{ 83 ObjectMeta: metav1.ObjectMeta{ 84 Name: "test", 85 Labels: map[string]string{"somelabel": "somevalue"}, 86 OwnerReferences: []metav1.OwnerReference{{ 87 Name: "controller", 88 }}, 89 }, 90 }, 91 modified: &appsv1.Deployment{ 92 ObjectMeta: metav1.ObjectMeta{ 93 Name: "somethingelse", 94 Labels: map[string]string{"somelabel": "anothervalue"}, 95 OwnerReferences: []metav1.OwnerReference{{ 96 Name: "another", 97 }}, 98 }, 99 }, 100 want: true, 101 options: []EqualObjectOption{ 102 IgnorePaths{ 103 "metadata.name", 104 "metadata.creationTimestamp", 105 "metadata.labels.somelabel", 106 "metadata.ownerReferences[0].name", 107 "spec.template.metadata", 108 }, 109 }, 110 }, 111 { 112 name: "succeeds if all allowed fields match, and some others do not", 113 original: &appsv1.Deployment{ 114 ObjectMeta: metav1.ObjectMeta{ 115 Name: "test", 116 Namespace: "default", 117 }, 118 }, 119 modified: &appsv1.Deployment{ 120 ObjectMeta: metav1.ObjectMeta{ 121 Name: "test", 122 Namespace: "special", 123 }, 124 }, 125 want: true, 126 options: []EqualObjectOption{ 127 MatchPaths{ 128 "ObjectMeta.Name", 129 }, 130 }, 131 }, 132 { 133 name: "works with unstructured.Unstructured", 134 original: &unstructured.Unstructured{ 135 Object: map[string]interface{}{ 136 "metadata": map[string]interface{}{ 137 "name": "something", 138 "namespace": "test", 139 }, 140 }, 141 }, 142 modified: &unstructured.Unstructured{ 143 Object: map[string]interface{}{ 144 "metadata": map[string]interface{}{ 145 "name": "somethingelse", 146 "namespace": "test", 147 }, 148 }, 149 }, 150 want: true, 151 options: []EqualObjectOption{ 152 IgnorePaths{ 153 "metadata.name", 154 }, 155 }, 156 }, 157 158 // Test when objects are equal. 159 { 160 name: "Equal field (spec) both in original and in modified", 161 original: &unstructured.Unstructured{ 162 Object: map[string]interface{}{ 163 "spec": map[string]interface{}{ 164 "foo": "bar", 165 }, 166 }, 167 }, 168 modified: &unstructured.Unstructured{ 169 Object: map[string]interface{}{ 170 "spec": map[string]interface{}{ 171 "foo": "bar", 172 }, 173 }, 174 }, 175 want: true, 176 }, 177 178 { 179 name: "Equal nested field both in original and in modified", 180 original: &unstructured.Unstructured{ 181 Object: map[string]interface{}{ 182 "spec": map[string]interface{}{ 183 "template": map[string]interface{}{ 184 "spec": map[string]interface{}{ 185 "A": "A", 186 }, 187 }, 188 }, 189 }, 190 }, 191 modified: &unstructured.Unstructured{ 192 Object: map[string]interface{}{ 193 "spec": map[string]interface{}{ 194 "template": map[string]interface{}{ 195 "spec": map[string]interface{}{ 196 "A": "A", 197 }, 198 }, 199 }, 200 }, 201 }, 202 want: true, 203 }, 204 205 // Test when there is a difference between the objects. 206 { 207 name: "Unequal field both in original and in modified", 208 original: &unstructured.Unstructured{ 209 Object: map[string]interface{}{ 210 "spec": map[string]interface{}{ 211 "foo": "bar-changed", 212 }, 213 }, 214 }, 215 modified: &unstructured.Unstructured{ 216 Object: map[string]interface{}{ 217 "spec": map[string]interface{}{ 218 "foo": "bar", 219 }, 220 }, 221 }, 222 want: false, 223 }, 224 { 225 name: "Unequal nested field both in original and modified", 226 original: &unstructured.Unstructured{ 227 Object: map[string]interface{}{ 228 "spec": map[string]interface{}{ 229 "template": map[string]interface{}{ 230 "spec": map[string]interface{}{ 231 "A": "A-Changed", 232 }, 233 }, 234 }, 235 }, 236 }, 237 modified: &unstructured.Unstructured{ 238 Object: map[string]interface{}{ 239 "spec": map[string]interface{}{ 240 "template": map[string]interface{}{ 241 "spec": map[string]interface{}{ 242 "A": "A", 243 }, 244 }, 245 }, 246 }, 247 }, 248 want: false, 249 }, 250 251 { 252 name: "Value of type map with different values", 253 original: &unstructured.Unstructured{ 254 Object: map[string]interface{}{ 255 "spec": map[string]interface{}{ 256 "map": map[string]string{ 257 "A": "A-changed", 258 "B": "B", 259 // C missing 260 }, 261 }, 262 }, 263 }, 264 modified: &unstructured.Unstructured{ 265 Object: map[string]interface{}{ 266 "spec": map[string]interface{}{ 267 "map": map[string]string{ 268 "A": "A", 269 // B missing 270 "C": "C", 271 }, 272 }, 273 }, 274 }, 275 want: false, 276 }, 277 278 { 279 name: "Value of type Array or Slice with same length but different values", 280 original: &unstructured.Unstructured{ 281 Object: map[string]interface{}{ 282 "spec": map[string]interface{}{ 283 "slice": []string{ 284 "D", 285 "C", 286 "B", 287 }, 288 }, 289 }, 290 }, 291 modified: &unstructured.Unstructured{ 292 Object: map[string]interface{}{ 293 "spec": map[string]interface{}{ 294 "slice": []string{ 295 "A", 296 "B", 297 "C", 298 }, 299 }, 300 }, 301 }, 302 want: false, 303 }, 304 305 // This tests specific behaviour in how Kubernetes marshals the zero value of metav1.Time{}. 306 { 307 name: "Creation timestamp set to empty value on both original and modified", 308 original: &unstructured.Unstructured{ 309 Object: map[string]interface{}{ 310 "spec": map[string]interface{}{ 311 "A": "A", 312 }, 313 "metadata": map[string]interface{}{ 314 "selfLink": "foo", 315 "creationTimestamp": metav1.Time{}, 316 }, 317 }, 318 }, 319 modified: &unstructured.Unstructured{ 320 Object: map[string]interface{}{ 321 "spec": map[string]interface{}{ 322 "A": "A", 323 }, 324 "metadata": map[string]interface{}{ 325 "selfLink": "foo", 326 "creationTimestamp": metav1.Time{}, 327 }, 328 }, 329 }, 330 want: true, 331 }, 332 333 // Cases to test diff when fields exist only in modified object. 334 { 335 name: "Field only in modified", 336 original: &unstructured.Unstructured{ 337 Object: map[string]interface{}{}, 338 }, 339 modified: &unstructured.Unstructured{ 340 Object: map[string]interface{}{ 341 "spec": map[string]interface{}{ 342 "foo": "bar", 343 }, 344 }, 345 }, 346 want: false, 347 }, 348 { 349 name: "Nested field only in modified", 350 original: &unstructured.Unstructured{ 351 Object: map[string]interface{}{}, 352 }, 353 modified: &unstructured.Unstructured{ 354 Object: map[string]interface{}{ 355 "spec": map[string]interface{}{ 356 "template": map[string]interface{}{ 357 "spec": map[string]interface{}{ 358 "A": "A", 359 }, 360 }, 361 }, 362 }, 363 }, 364 want: false, 365 }, 366 { 367 name: "Creation timestamp exists on modified but not on original", 368 original: &unstructured.Unstructured{ 369 Object: map[string]interface{}{ 370 "spec": map[string]interface{}{ 371 "A": "A", 372 }, 373 }, 374 }, 375 modified: &unstructured.Unstructured{ 376 Object: map[string]interface{}{ 377 "spec": map[string]interface{}{ 378 "A": "A", 379 }, 380 "metadata": map[string]interface{}{ 381 "selfLink": "foo", 382 "creationTimestamp": "2021-11-03T11:05:17Z", 383 }, 384 }, 385 }, 386 want: false, 387 }, 388 389 // Test when fields exists only in the original object. 390 { 391 name: "Field only in original", 392 original: &unstructured.Unstructured{ 393 Object: map[string]interface{}{ 394 "spec": map[string]interface{}{ 395 "foo": "bar", 396 }, 397 }, 398 }, 399 modified: &unstructured.Unstructured{ 400 Object: map[string]interface{}{}, 401 }, 402 want: false, 403 }, 404 { 405 name: "Nested field only in original", 406 original: &unstructured.Unstructured{ 407 Object: map[string]interface{}{ 408 "spec": map[string]interface{}{ 409 "template": map[string]interface{}{ 410 "spec": map[string]interface{}{ 411 "A": "A", 412 }, 413 }, 414 }, 415 }, 416 }, 417 modified: &unstructured.Unstructured{ 418 Object: map[string]interface{}{}, 419 }, 420 want: false, 421 }, 422 { 423 name: "Creation timestamp exists on original but not on modified", 424 original: &unstructured.Unstructured{ 425 Object: map[string]interface{}{ 426 "spec": map[string]interface{}{ 427 "A": "A", 428 }, 429 "metadata": map[string]interface{}{ 430 "selfLink": "foo", 431 "creationTimestamp": "2021-11-03T11:05:17Z", 432 }, 433 }, 434 }, 435 modified: &unstructured.Unstructured{ 436 Object: map[string]interface{}{ 437 "spec": map[string]interface{}{ 438 "A": "A", 439 }, 440 }, 441 }, 442 443 want: false, 444 }, 445 446 // Test metadata fields computed by the system or in status are compared. 447 { 448 name: "Unequal Metadata fields computed by the system or in status", 449 original: &unstructured.Unstructured{ 450 Object: map[string]interface{}{}, 451 }, 452 modified: &unstructured.Unstructured{ 453 Object: map[string]interface{}{ 454 "metadata": map[string]interface{}{ 455 "selfLink": "foo", 456 "uid": "foo", 457 "resourceVersion": "foo", 458 "generation": "foo", 459 "managedFields": "foo", 460 }, 461 "status": map[string]interface{}{ 462 "foo": "bar", 463 }, 464 }, 465 }, 466 want: false, 467 }, 468 { 469 name: "Unequal labels and annotations", 470 original: &unstructured.Unstructured{ 471 Object: map[string]interface{}{}, 472 }, 473 modified: &unstructured.Unstructured{ 474 Object: map[string]interface{}{ 475 "metadata": map[string]interface{}{ 476 "labels": map[string]interface{}{ 477 "foo": "bar", 478 }, 479 "annotations": map[string]interface{}{ 480 "foo": "bar", 481 }, 482 }, 483 }, 484 }, 485 want: false, 486 }, 487 488 // Ignore fields MatchOption 489 { 490 name: "Unequal metadata fields ignored by IgnorePaths MatchOption", 491 original: &unstructured.Unstructured{ 492 Object: map[string]interface{}{ 493 "metadata": map[string]interface{}{ 494 "name": "test", 495 }, 496 }, 497 }, 498 modified: &unstructured.Unstructured{ 499 Object: map[string]interface{}{ 500 "metadata": map[string]interface{}{ 501 "name": "test", 502 "selfLink": "foo", 503 "uid": "foo", 504 "resourceVersion": "foo", 505 "generation": "foo", 506 "managedFields": "foo", 507 }, 508 }, 509 }, 510 options: []EqualObjectOption{IgnoreAutogeneratedMetadata}, 511 want: true, 512 }, 513 { 514 name: "Unequal labels and annotations ignored by IgnorePaths MatchOption", 515 original: &unstructured.Unstructured{ 516 Object: map[string]interface{}{ 517 "metadata": map[string]interface{}{ 518 "name": "test", 519 }, 520 }, 521 }, 522 modified: &unstructured.Unstructured{ 523 Object: map[string]interface{}{ 524 "metadata": map[string]interface{}{ 525 "name": "test", 526 "labels": map[string]interface{}{ 527 "foo": "bar", 528 }, 529 "annotations": map[string]interface{}{ 530 "foo": "bar", 531 }, 532 }, 533 }, 534 }, 535 options: []EqualObjectOption{IgnorePaths{"metadata.labels", "metadata.annotations"}}, 536 want: true, 537 }, 538 { 539 name: "Ignore fields are not compared", 540 original: &unstructured.Unstructured{ 541 Object: map[string]interface{}{ 542 "spec": map[string]interface{}{}, 543 }, 544 }, 545 modified: &unstructured.Unstructured{ 546 Object: map[string]interface{}{ 547 "spec": map[string]interface{}{ 548 "controlPlaneEndpoint": map[string]interface{}{ 549 "host": "", 550 "port": 0, 551 }, 552 }, 553 }, 554 }, 555 options: []EqualObjectOption{IgnorePaths{"spec.controlPlaneEndpoint"}}, 556 want: true, 557 }, 558 { 559 name: "Not-ignored fields are still compared", 560 original: &unstructured.Unstructured{ 561 Object: map[string]interface{}{ 562 "metadata": map[string]interface{}{ 563 "annotations": map[string]interface{}{}, 564 }, 565 }, 566 }, 567 modified: &unstructured.Unstructured{ 568 Object: map[string]interface{}{ 569 "metadata": map[string]interface{}{ 570 "annotations": map[string]interface{}{ 571 "ignored": "somevalue", 572 "superflous": "shouldcausefailure", 573 }, 574 }, 575 }, 576 }, 577 options: []EqualObjectOption{IgnorePaths{"metadata.annotations.ignored"}}, 578 want: false, 579 }, 580 581 // MatchPaths MatchOption 582 { 583 name: "Unequal metadata fields not compared by setting MatchPaths MatchOption", 584 original: &unstructured.Unstructured{ 585 Object: map[string]interface{}{ 586 "spec": map[string]interface{}{ 587 "A": "A", 588 }, 589 }, 590 }, 591 modified: &unstructured.Unstructured{ 592 Object: map[string]interface{}{ 593 "spec": map[string]interface{}{ 594 "A": "A", 595 }, 596 "metadata": map[string]interface{}{ 597 "selfLink": "foo", 598 "uid": "foo", 599 }, 600 }, 601 }, 602 options: []EqualObjectOption{MatchPaths{"spec"}}, 603 want: true, 604 }, 605 606 // More tests 607 { 608 name: "No changes", 609 original: &unstructured.Unstructured{ 610 Object: map[string]interface{}{ 611 "spec": map[string]interface{}{ 612 "A": "A", 613 "B": "B", 614 "C": "C", // C only in original 615 }, 616 }, 617 }, 618 modified: &unstructured.Unstructured{ 619 Object: map[string]interface{}{ 620 "spec": map[string]interface{}{ 621 "A": "A", 622 "B": "B", 623 }, 624 }, 625 }, 626 want: false, 627 }, 628 { 629 name: "Many changes", 630 original: &unstructured.Unstructured{ 631 Object: map[string]interface{}{ 632 "spec": map[string]interface{}{ 633 "A": "A", 634 // B missing 635 "C": "C", // C only in original 636 }, 637 }, 638 }, 639 modified: &unstructured.Unstructured{ 640 Object: map[string]interface{}{ 641 "spec": map[string]interface{}{ 642 "A": "A", 643 "B": "B", 644 }, 645 }, 646 }, 647 want: false, 648 }, 649 } 650 651 for _, c := range cases { 652 t.Run(c.name, func(t *testing.T) { 653 g := NewWithT(t) 654 m := EqualObject(c.original, c.options...) 655 success, _ := m.Match(c.modified) 656 if !success { 657 t.Log(m.FailureMessage(c.modified)) 658 } 659 g.Expect(success).To(Equal(c.want)) 660 }) 661 } 662 }