github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/openshift/changeset_test.go (about) 1 package openshift 2 3 import ( 4 "strings" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/opendevstack/tailor/internal/test/helper" 9 ) 10 11 func TestNewChangesetCreationOfResources(t *testing.T) { 12 tests := map[string]struct { 13 templateFixture string 14 expectedGolden string 15 }{ 16 "Without annotations": { 17 templateFixture: "is.yml", 18 expectedGolden: "is.yml", 19 }, 20 "With annotations": { 21 templateFixture: "is-annotation.yml", 22 expectedGolden: "is-annotation.yml", 23 }, 24 "With image reference": { 25 templateFixture: "dc.yml", 26 expectedGolden: "dc.yml", 27 }, 28 "With image reference and annotation": { 29 templateFixture: "dc-annotation.yml", 30 expectedGolden: "dc-annotation.yml", 31 }, 32 } 33 34 for name, tc := range tests { 35 t.Run(name, func(t *testing.T) { 36 filter, err := NewResourceFilter("", "", []string{}) 37 if err != nil { 38 t.Fatal(err) 39 } 40 platformBasedList, err := NewPlatformBasedResourceList( 41 filter, 42 []byte(""), // empty to ensure creation of resource 43 ) 44 if err != nil { 45 t.Fatal(err) 46 } 47 templateBasedList, err := NewTemplateBasedResourceList( 48 filter, 49 helper.ReadFixtureFile(t, "templates/"+tc.templateFixture), 50 ) 51 if err != nil { 52 t.Fatal(err) 53 } 54 upsertOnly := false 55 allowRecreate := false 56 preservePaths := []string{} 57 cs, err := NewChangeset( 58 platformBasedList, 59 templateBasedList, 60 upsertOnly, 61 allowRecreate, 62 preservePaths, 63 ) 64 if err != nil { 65 t.Fatal(err) 66 } 67 createChanges := cs.Create 68 numberOfCreateChanges := len(createChanges) 69 if numberOfCreateChanges != 1 { 70 t.Fatalf("Expected one creation change, got: %d", numberOfCreateChanges) 71 } 72 createChange := createChanges[0] 73 want := string(helper.ReadGoldenFile(t, "desired-state/"+tc.expectedGolden)) 74 got := createChange.DesiredState 75 if diff := cmp.Diff(want, got); diff != "" { 76 t.Fatalf("Desired state mismatch (-want +got):\n%s", diff) 77 } 78 }) 79 } 80 } 81 82 func TestCalculateChangesManagedAnnotations(t *testing.T) { 83 84 tests := map[string]struct { 85 platformFixture string 86 templateFixture string 87 expectedAction string 88 expectedDiffGoldenFile string 89 }{ 90 "Without annotations": { 91 platformFixture: "is-platform", 92 templateFixture: "is-template", 93 expectedAction: "Noop", 94 }, 95 "Present in template, not in platform": { 96 platformFixture: "is-platform", 97 templateFixture: "is-template-annotation", 98 expectedAction: "Update", 99 expectedDiffGoldenFile: "present-in-template-not-in-platform", 100 }, 101 "Present in platform, not in template": { 102 platformFixture: "is-platform-annotation", 103 templateFixture: "is-template", 104 expectedAction: "Update", 105 expectedDiffGoldenFile: "present-in-platform-not-in-template", 106 }, 107 "Present in both": { 108 platformFixture: "is-platform-annotation", 109 templateFixture: "is-template-annotation", 110 expectedAction: "Noop", 111 }, 112 "Present in platform, changed in template": { 113 platformFixture: "is-platform-annotation", 114 templateFixture: "is-template-annotation-changed", 115 expectedAction: "Update", 116 expectedDiffGoldenFile: "present-in-platform-changed-in-template", 117 }, 118 "Present in platform, different key in template": { 119 platformFixture: "is-platform-annotation", 120 templateFixture: "is-template-different-annotation", 121 expectedAction: "Update", 122 expectedDiffGoldenFile: "present-in-platform-different-key-in-template", 123 }, 124 "Unmanaged in platform added to template": { 125 platformFixture: "is-platform-unmanaged", 126 templateFixture: "is-template-annotation", 127 expectedAction: "Noop", 128 }, 129 "Unmanaged in platform, none in template": { 130 platformFixture: "is-platform-unmanaged", 131 templateFixture: "is-template", 132 expectedAction: "Noop", 133 }, 134 "Unmanaged in platform, none in template, and other change in template": { 135 platformFixture: "is-platform-unmanaged", 136 templateFixture: "is-template-other-change", 137 expectedAction: "Update", 138 expectedDiffGoldenFile: "unmanaged-in-platform-none-in-template-other-change-in-template", 139 }, 140 } 141 142 for name, tc := range tests { 143 t.Run(name, func(t *testing.T) { 144 platformItem := getPlatformItem(t, "item-managed-annotations/"+tc.platformFixture+".yml") 145 templateItem := getTemplateItem(t, "item-managed-annotations/"+tc.templateFixture+".yml") 146 changes, err := calculateChanges(templateItem, platformItem, []string{}, true) 147 if err != nil { 148 t.Fatal(err) 149 } 150 if len(changes) != 1 { 151 t.Fatalf("Expected 1 change, got: %d", len(changes)) 152 } 153 actualChange := changes[0] 154 if actualChange.Action != tc.expectedAction { 155 t.Fatalf("Expected change action to be: %s, got: %s", tc.expectedAction, actualChange.Action) 156 } 157 if len(tc.expectedDiffGoldenFile) > 0 { 158 want := strings.TrimSpace(getGoldenDiff(t, "item-managed-annotations", tc.expectedDiffGoldenFile+".txt")) 159 got := strings.TrimSpace(actualChange.Diff(true)) 160 if diff := cmp.Diff(want, got); diff != "" { 161 t.Errorf("Change diff mismatch (-want +got):\n%s", diff) 162 } 163 } 164 }) 165 } 166 } 167 168 func TestCalculateChangesAppliedConfiguration(t *testing.T) { 169 170 tests := map[string]struct { 171 platformFixture string 172 templateFixture string 173 expectedAction string 174 }{ 175 "Without annotation in platform": { 176 platformFixture: "dc-platform", 177 templateFixture: "dc-template", 178 expectedAction: "Update", 179 }, 180 "With annotation in platform": { 181 platformFixture: "dc-platform-annotation-other", 182 templateFixture: "dc-template", 183 expectedAction: "Update", 184 }, 185 "Present in platform": { 186 platformFixture: "dc-platform-annotation-applied", 187 templateFixture: "dc-template", 188 expectedAction: "Noop", 189 }, 190 "Old Tailor annotation present in platform": { 191 platformFixture: "dc-platform-annotation-tailor", 192 templateFixture: "dc-template", 193 expectedAction: "Noop", 194 }, 195 "Present in platform, changed in template": { 196 platformFixture: "dc-platform-annotation-applied", 197 templateFixture: "dc-template-changed", 198 expectedAction: "Update", 199 }, 200 } 201 202 for name, tc := range tests { 203 t.Run(name, func(t *testing.T) { 204 platformItem := getPlatformItem(t, "item-applied-config/"+tc.platformFixture+".yml") 205 templateItem := getTemplateItem(t, "item-applied-config/"+tc.templateFixture+".yml") 206 changes, err := calculateChanges(templateItem, platformItem, []string{}, true) 207 if err != nil { 208 t.Fatal(err) 209 } 210 if len(changes) != 1 { 211 t.Fatalf("Expected 1 change, got: %d", len(changes)) 212 } 213 actualChange := changes[0] 214 if actualChange.Action != tc.expectedAction { 215 t.Fatalf("Expected change action to be: %s, got: %s. Diff:\n%s", tc.expectedAction, actualChange.Action, actualChange.Diff(true)) 216 } 217 }) 218 } 219 } 220 221 func TestCalculateChangesOmittedFields(t *testing.T) { 222 223 tests := map[string]struct { 224 platformFixture string 225 templateFixture string 226 expectedAction string 227 expectedDiffGoldenFile string 228 }{ 229 "Rolebinding with legacy fields": { 230 platformFixture: "rolebinding-platform", 231 templateFixture: "rolebinding-template", 232 expectedAction: "Update", 233 expectedDiffGoldenFile: "rolebinding-changed", 234 }, 235 } 236 237 for name, tc := range tests { 238 t.Run(name, func(t *testing.T) { 239 platformItem := getPlatformItem(t, "item-omitted-fields/"+tc.platformFixture+".yml") 240 templateItem := getTemplateItem(t, "item-omitted-fields/"+tc.templateFixture+".yml") 241 changes, err := calculateChanges(templateItem, platformItem, []string{}, true) 242 if err != nil { 243 t.Fatal(err) 244 } 245 if len(changes) != 1 { 246 t.Fatalf("Expected 1 change, got: %d", len(changes)) 247 } 248 actualChange := changes[0] 249 if actualChange.Action != tc.expectedAction { 250 t.Fatalf("Expected change action to be: %s, got: %s", tc.expectedAction, actualChange.Action) 251 } 252 if len(tc.expectedDiffGoldenFile) > 0 { 253 want := strings.TrimSpace(getGoldenDiff(t, "item-omitted-fields", tc.expectedDiffGoldenFile+".txt")) 254 got := strings.TrimSpace(actualChange.Diff(true)) 255 if diff := cmp.Diff(want, got); diff != "" { 256 t.Errorf("Change diff mismatch (-want +got):\n%s", diff) 257 } 258 } 259 }) 260 } 261 } 262 263 func TestEmptyValuesDoNotCauseDrift(t *testing.T) { 264 265 tests := map[string]struct { 266 platformFixture string 267 templateFixture string 268 expectedAction string 269 }{ 270 "Field not defined in template": { 271 platformFixture: "bc-platform-defaulted.yml", 272 templateFixture: "bc-template-defaulted.yml", 273 expectedAction: "Noop", 274 }, 275 "Field not set in platform, and empty in template": { 276 platformFixture: "bc-platform-missing-env.yml", 277 templateFixture: "bc-template-empty-env.yml", 278 expectedAction: "Noop", 279 }, 280 } 281 282 for name, tc := range tests { 283 t.Run(name, func(t *testing.T) { 284 platformItem := getPlatformItem(t, "empty-values/"+tc.platformFixture) 285 templateItem := getTemplateItem(t, "empty-values/"+tc.templateFixture) 286 changes, err := calculateChanges(templateItem, platformItem, []string{}, true) 287 if err != nil { 288 t.Fatal(err) 289 } 290 if len(changes) != 1 { 291 t.Fatalf("Expected 1 change, got: %d", len(changes)) 292 } 293 actualChange := changes[0] 294 if actualChange.Action != tc.expectedAction { 295 t.Fatalf("Expected change action to be: %s, got: %s. Diff was: %s", tc.expectedAction, actualChange.Action, actualChange.Diff(false)) 296 } 297 }) 298 } 299 } 300 301 func TestAddCreateOrder(t *testing.T) { 302 cs := fillChangeset("Create") 303 if cs.Create[0].Kind != "ServiceAccount" { 304 t.Errorf("SA needs to be created before PVC") 305 } 306 if cs.Create[1].Kind != "PersistentVolumeClaim" { 307 t.Errorf("PVC needs to be created before DC") 308 } 309 } 310 311 func TestAddUpdateOrder(t *testing.T) { 312 cs := fillChangeset("Update") 313 if cs.Update[0].Kind != "ServiceAccount" { 314 t.Errorf("SA needs to be created before PVC") 315 } 316 if cs.Update[1].Kind != "PersistentVolumeClaim" { 317 t.Errorf("PVC needs to be updated before DC") 318 } 319 } 320 321 func TestAddDeleteOrder(t *testing.T) { 322 cs := fillChangeset("Delete") 323 if cs.Delete[0].Kind != "DeploymentConfig" { 324 t.Errorf("DC needs to be deleted before PVC") 325 } 326 if cs.Delete[1].Kind != "PersistentVolumeClaim" { 327 t.Errorf("PVC needs to be deleted before SA") 328 } 329 } 330 331 func fillChangeset(action string) *Changeset { 332 cs := &Changeset{} 333 cDC := &Change{ 334 Action: action, 335 Kind: "DeploymentConfig", 336 } 337 cPVC := &Change{ 338 Action: action, 339 Kind: "PersistentVolumeClaim", 340 } 341 cSA := &Change{ 342 Action: action, 343 Kind: "ServiceAccount", 344 } 345 cs.Add(cPVC, cDC, cSA) 346 return cs 347 } 348 349 func TestConfigNoop(t *testing.T) { 350 351 templateInput := []byte( 352 `kind: List 353 metadata: {} 354 apiVersion: v1 355 items: 356 - apiVersion: v1 357 kind: PersistentVolumeClaim 358 metadata: 359 labels: 360 template: foo-template 361 name: foo 362 spec: 363 accessModes: 364 - ReadWriteOnce 365 resources: 366 requests: 367 storage: 5Gi 368 storageClassName: gp2 369 status: {}`) 370 371 platformInput := []byte( 372 `kind: List 373 metadata: {} 374 apiVersion: v1 375 items: 376 - apiVersion: v1 377 kind: PersistentVolumeClaim 378 metadata: 379 annotations: 380 pv.kubernetes.io/bind-completed: "yes" 381 pv.kubernetes.io/bound-by-controller: "yes" 382 volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/aws-ebs 383 labels: 384 template: foo-template 385 name: foo 386 spec: 387 accessModes: 388 - ReadWriteOnce 389 resources: 390 requests: 391 storage: 5Gi 392 storageClassName: gp2 393 volumeName: pvc-2150713e-3e20-11e8-aa60-0aad3152d0e6 394 status: {}`) 395 396 filter := &ResourceFilter{ 397 Kinds: []string{"PersistentVolumeClaim"}, 398 } 399 changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{}) 400 if !changeset.Blank() { 401 t.Fatalf("Changeset is not blank!") 402 } 403 } 404 405 func TestConfigUpdate(t *testing.T) { 406 407 templateInput := []byte( 408 `kind: List 409 metadata: {} 410 apiVersion: v1 411 items: 412 - apiVersion: v1 413 kind: PersistentVolumeClaim 414 metadata: 415 name: foo 416 labels: 417 app: foo 418 spec: 419 accessModes: 420 - ReadWriteOnce 421 resources: 422 requests: 423 storage: 5Gi 424 storageClassName: gp2 425 status: {}`) 426 427 platformInput := []byte( 428 `kind: List 429 metadata: {} 430 apiVersion: v1 431 items: 432 - apiVersion: v1 433 kind: PersistentVolumeClaim 434 metadata: 435 name: foo 436 annotations: 437 kubectl.kubernetes.io/last-applied-configuration: > 438 {"apiVersion":"1"} 439 spec: 440 accessModes: 441 - ReadWriteOnce 442 resources: 443 requests: 444 storage: 5Gi 445 storageClassName: gp2 446 status: {}`) 447 448 filter := &ResourceFilter{ 449 Kinds: []string{"PersistentVolumeClaim"}, 450 } 451 changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{}) 452 if len(changeset.Update) != 1 { 453 t.Errorf("Changeset.Update has %d items instead of 1", len(changeset.Update)) 454 } 455 } 456 457 func TestConfigPreservePaths(t *testing.T) { 458 templateInput := []byte( 459 `kind: List 460 apiVersion: v1 461 items: 462 - apiVersion: v1 463 kind: BuildConfig 464 metadata: 465 name: foo 466 spec: 467 failedBuildsHistoryLimit: 5 468 output: 469 to: 470 kind: ImageStreamTag 471 name: foo:latest 472 postCommit: {} 473 resources: {} 474 runPolicy: Serial 475 source: 476 binary: {} 477 type: Binary 478 strategy: 479 dockerStrategy: {} 480 type: Docker 481 successfulBuildsHistoryLimit: 5 482 triggers: 483 - generic: 484 secret: password 485 type: Generic`) 486 487 platformInput := []byte( 488 `kind: List 489 apiVersion: v1 490 items: 491 - apiVersion: v1 492 kind: BuildConfig 493 metadata: 494 name: foo 495 spec: 496 failedBuildsHistoryLimit: 5 497 output: 498 to: 499 kind: ImageStreamTag 500 name: foo:abcdef 501 imageLabels: 502 - name: bar 503 value: baz 504 postCommit: {} 505 resources: {} 506 runPolicy: Serial 507 source: 508 binary: {} 509 type: Binary 510 strategy: 511 dockerStrategy: {} 512 type: Docker 513 successfulBuildsHistoryLimit: 5 514 triggers: 515 - generic: 516 secret: password 517 type: Generic`) 518 519 filter := &ResourceFilter{ 520 Kinds: []string{"BuildConfig"}, 521 } 522 changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{"bc:/spec/output/to/name", "bc:/spec/output/imageLabels"}) 523 actualUpdates := len(changeset.Update) 524 expectedUpdates := 0 525 if actualUpdates != expectedUpdates { 526 t.Errorf("Changeset.Update has %d items instead of %d", actualUpdates, expectedUpdates) 527 } 528 } 529 530 func TestConfigCreation(t *testing.T) { 531 templateInput := []byte( 532 `kind: List 533 metadata: {} 534 apiVersion: v1 535 items: 536 - apiVersion: v1 537 kind: PersistentVolumeClaim 538 metadata: 539 name: foo 540 spec: 541 accessModes: 542 - ReadWriteOnce 543 resources: 544 requests: 545 storage: 5Gi 546 storageClassName: gp2 547 status: {}`) 548 549 platformInput := []byte( 550 `kind: List 551 metadata: {} 552 apiVersion: v1 553 items: 554 - apiVersion: v1 555 kind: PersistentVolumeClaim 556 metadata: 557 name: bar 558 spec: 559 accessModes: 560 - ReadWriteOnce 561 resources: 562 requests: 563 storage: 5Gi 564 storageClassName: gp2 565 status: {}`) 566 567 filter := &ResourceFilter{ 568 Kinds: []string{"PersistentVolumeClaim"}, 569 } 570 changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{}) 571 if len(changeset.Create) != 1 { 572 t.Errorf("Changeset.Create is blank but should not be") 573 } 574 } 575 576 func TestConfigDeletion(t *testing.T) { 577 578 templateInput := []byte{} 579 580 platformInput := []byte( 581 `kind: List 582 metadata: {} 583 apiVersion: v1 584 items: 585 - apiVersion: v1 586 kind: PersistentVolumeClaim 587 metadata: 588 name: foo 589 spec: 590 accessModes: 591 - ReadWriteOnce 592 resources: 593 requests: 594 storage: 5Gi 595 storageClassName: gp2 596 status: {}`) 597 598 filter := &ResourceFilter{ 599 Kinds: []string{"PersistentVolumeClaim"}, 600 } 601 changeset := getChangeset(t, filter, platformInput, templateInput, false, true, []string{}) 602 if len(changeset.Delete) != 1 { 603 t.Errorf("Changeset.Delete is blank but should not be") 604 } 605 } 606 607 func TestCalculateChangesEqual(t *testing.T) { 608 currentItem := getItem(t, getBuildConfig(), "platform") 609 desiredItem := getItem(t, getBuildConfig(), "template") 610 _, err := calculateChanges(desiredItem, currentItem, []string{}, true) 611 if err != nil { 612 t.Errorf(err.Error()) 613 } 614 } 615 616 func TestCalculateChangesImmutableFields(t *testing.T) { 617 platformItem := getItem(t, getRoute([]byte("old.com")), "platform") 618 619 unchangedTemplateItem := getItem(t, getRoute([]byte("old.com")), "template") 620 changes, err := calculateChanges(unchangedTemplateItem, platformItem, []string{}, true) 621 if err != nil { 622 t.Errorf(err.Error()) 623 } 624 if len(changes) > 1 || changes[0].Action != "Noop" { 625 t.Errorf("Platform and template should be in sync, got %d change(s): %v", len(changes), changes[0]) 626 } 627 628 changedTemplateItem := getItem(t, getRoute([]byte("new.com")), "template") 629 changes, err = calculateChanges(changedTemplateItem, platformItem, []string{}, true) 630 if err != nil { 631 t.Errorf(err.Error()) 632 } 633 if len(changes) == 0 { 634 t.Errorf("Platform and template should have drift.") 635 } 636 } 637 638 func getChangeset(t *testing.T, filter *ResourceFilter, platformInput, templateInput []byte, upsertOnly bool, allowRecreate bool, preservePaths []string) *Changeset { 639 platformBasedList, err := NewPlatformBasedResourceList(filter, platformInput) 640 if err != nil { 641 t.Error("Could not create platform based list:", err) 642 } 643 templateBasedList, err := NewTemplateBasedResourceList(filter, templateInput) 644 if err != nil { 645 t.Error("Could not create template based list:", err) 646 } 647 changeset, err := NewChangeset(platformBasedList, templateBasedList, upsertOnly, allowRecreate, preservePaths) 648 if err != nil { 649 t.Error("Could not create changeset:", err) 650 } 651 return changeset 652 } 653 654 func getGoldenDiff(t *testing.T, folder string, filename string) string { 655 b := helper.ReadGoldenFile(t, folder+"/"+filename) 656 return string(b) 657 }