github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/merge/merge3_test.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package merge_test 16 17 import ( 18 "os" 19 "path/filepath" 20 "strings" 21 "testing" 22 23 "github.com/GoogleContainerTools/kpt/internal/testutil" 24 "github.com/GoogleContainerTools/kpt/internal/testutil/pkgbuilder" 25 "github.com/GoogleContainerTools/kpt/internal/util/merge" 26 "github.com/stretchr/testify/assert" 27 "sigs.k8s.io/kustomize/kyaml/copyutil" 28 "sigs.k8s.io/kustomize/kyaml/yaml" 29 ) 30 31 func TestMerge3_Nested_packages(t *testing.T) { 32 annotationSetter := yaml.SetAnnotation("foo", "bar") 33 labelSetter := yaml.SetLabel("bar", "foo") 34 35 testCases := []struct { 36 name string 37 includeSubPackages bool 38 original *pkgbuilder.RootPkg 39 upstream *pkgbuilder.RootPkg 40 local *pkgbuilder.RootPkg 41 expected *pkgbuilder.RootPkg 42 }{ 43 { 44 name: "subpackages are merged if included", 45 includeSubPackages: true, 46 original: createPkg(), 47 upstream: createPkg(annotationSetter), 48 local: createPkg(labelSetter), 49 expected: createPkg(labelSetter, annotationSetter), 50 }, 51 { 52 name: "subpackages are not merged if not included", 53 includeSubPackages: false, 54 original: createPkg(), 55 upstream: createPkg(annotationSetter), 56 local: createPkg(labelSetter), 57 expected: createPkgMultipleMutators( 58 []yaml.Filter{ 59 labelSetter, 60 annotationSetter, 61 }, 62 []yaml.Filter{ 63 labelSetter, 64 }, 65 ), 66 }, 67 { 68 name: "local copy defines the package boundaries if different from upstream", 69 includeSubPackages: false, 70 original: pkgbuilder.NewRootPkg(). 71 WithKptfile(). 72 WithResource(pkgbuilder.DeploymentResource). 73 WithSubPackages( 74 pkgbuilder.NewSubPkg("a"). 75 WithKptfile(). 76 WithResource(pkgbuilder.DeploymentResource), 77 ), 78 upstream: pkgbuilder.NewRootPkg(). 79 WithKptfile(). 80 WithResource(pkgbuilder.DeploymentResource, annotationSetter). 81 WithSubPackages( 82 pkgbuilder.NewSubPkg("a"). 83 WithResource(pkgbuilder.DeploymentResource, annotationSetter), 84 ), 85 local: pkgbuilder.NewRootPkg(). 86 WithKptfile(). 87 WithResource(pkgbuilder.DeploymentResource, labelSetter). 88 WithSubPackages( 89 pkgbuilder.NewSubPkg("a"). 90 WithKptfile(). 91 WithResource(pkgbuilder.DeploymentResource, labelSetter), 92 ), 93 expected: pkgbuilder.NewRootPkg(). 94 WithKptfile(). 95 WithResource(pkgbuilder.DeploymentResource, labelSetter, annotationSetter). 96 WithSubPackages( 97 pkgbuilder.NewSubPkg("a"). 98 WithKptfile(). 99 WithResource(pkgbuilder.DeploymentResource, labelSetter), 100 ), 101 }, 102 { 103 name: "upstream changes not included if in a different package", 104 includeSubPackages: false, 105 original: pkgbuilder.NewRootPkg(). 106 WithKptfile(). 107 WithResource(pkgbuilder.DeploymentResource). 108 WithSubPackages( 109 pkgbuilder.NewSubPkg("a"). 110 WithKptfile(). 111 WithResource(pkgbuilder.DeploymentResource), 112 ), 113 upstream: pkgbuilder.NewRootPkg(). 114 WithKptfile(). 115 WithResource(pkgbuilder.DeploymentResource, annotationSetter). 116 WithSubPackages( 117 pkgbuilder.NewSubPkg("a"). 118 WithKptfile(). 119 WithResource(pkgbuilder.DeploymentResource, annotationSetter), 120 ), 121 local: pkgbuilder.NewRootPkg(). 122 WithKptfile(). 123 WithResource(pkgbuilder.DeploymentResource, labelSetter). 124 WithSubPackages( 125 pkgbuilder.NewSubPkg("a"). // No Kptfile 126 WithResource(pkgbuilder.DeploymentResource, labelSetter), 127 ), 128 expected: pkgbuilder.NewRootPkg(). 129 WithKptfile(). 130 WithResource(pkgbuilder.DeploymentResource, labelSetter, annotationSetter). 131 WithSubPackages( 132 pkgbuilder.NewSubPkg("a"). 133 WithResource(pkgbuilder.DeploymentResource, labelSetter), 134 ), 135 }, 136 } 137 138 for i := range testCases { 139 test := testCases[i] 140 t.Run(test.name, func(t *testing.T) { 141 original := test.original.ExpandPkg(t, testutil.EmptyReposInfo) 142 updated := test.upstream.ExpandPkg(t, testutil.EmptyReposInfo) 143 local := test.local.ExpandPkg(t, testutil.EmptyReposInfo) 144 expected := test.expected.ExpandPkg(t, testutil.EmptyReposInfo) 145 err := merge.Merge3{ 146 OriginalPath: original, 147 UpdatedPath: updated, 148 DestPath: local, 149 MergeOnPath: true, 150 IncludeSubPackages: test.includeSubPackages, 151 }.Merge() 152 if !assert.NoError(t, err) { 153 t.FailNow() 154 } 155 156 diffs, err := copyutil.Diff(local, expected) 157 if !assert.NoError(t, err) { 158 t.FailNow() 159 } 160 161 if !assert.Empty(t, diffs.List()) { 162 t.FailNow() 163 } 164 }) 165 } 166 } 167 168 func createPkg(mutators ...yaml.Filter) *pkgbuilder.RootPkg { 169 return createPkgMultipleMutators(mutators, mutators) 170 } 171 172 func createPkgMultipleMutators(packageMutators, subPackageMutators []yaml.Filter) *pkgbuilder.RootPkg { 173 return pkgbuilder.NewRootPkg(). 174 WithKptfile(). 175 WithResource(pkgbuilder.DeploymentResource, packageMutators...). 176 WithSubPackages( 177 pkgbuilder.NewSubPkg("a"). 178 WithKptfile(). 179 WithResource(pkgbuilder.DeploymentResource, subPackageMutators...), 180 pkgbuilder.NewSubPkg("b"). 181 WithResource(pkgbuilder.DeploymentResource, packageMutators...). 182 WithSubPackages( 183 pkgbuilder.NewSubPkg("c"). 184 WithKptfile(). 185 WithResource(pkgbuilder.DeploymentResource, subPackageMutators...), 186 ), 187 ) 188 } 189 190 func TestMerge3_Merge_path(t *testing.T) { 191 testCases := map[string]struct { 192 origin string 193 update string 194 local string 195 expected string 196 errMsg string 197 }{ 198 `Most common: add namespace and name-prefix on local, merge upstream changes`: { 199 origin: ` 200 apiVersion: apps/v1 201 kind: Deployment 202 metadata: 203 name: nginx-deployment 204 spec: 205 replicas: 3`, 206 update: ` 207 apiVersion: apps/v1 208 kind: Deployment 209 metadata: 210 name: nginx-deployment 211 spec: 212 replicas: 4`, 213 local: ` 214 apiVersion: apps/v1 215 kind: Deployment 216 metadata: # kpt-merge: /nginx-deployment 217 name: dev-nginx-deployment 218 namespace: my-space 219 spec: 220 replicas: 3 221 `, 222 expected: ` 223 apiVersion: apps/v1 224 kind: Deployment 225 metadata: # kpt-merge: /nginx-deployment 226 name: dev-nginx-deployment 227 namespace: my-space 228 spec: 229 replicas: 4 230 `}, 231 232 `Add namespace and name-prefix on local manually without adding annotations, adds new resource`: { 233 origin: ` 234 apiVersion: apps/v1 235 kind: Deployment 236 metadata: 237 name: nginx-deployment 238 spec: 239 replicas: 3`, 240 update: ` 241 apiVersion: apps/v1 242 kind: Deployment 243 metadata: 244 name: nginx-deployment 245 spec: 246 replicas: 4`, 247 local: ` 248 apiVersion: apps/v1 249 kind: Deployment 250 metadata: 251 name: dev-nginx-deployment 252 namespace: my-space 253 spec: 254 replicas: 3 255 `, 256 expected: ` 257 apiVersion: apps/v1 258 kind: Deployment 259 metadata: 260 name: dev-nginx-deployment 261 namespace: my-space 262 spec: 263 replicas: 3 264 `}, 265 266 `Conflict: User fetches package, copies a resource in same file, adds different name suffix`: { 267 origin: ` 268 apiVersion: apps/v1 269 kind: Deployment 270 metadata: 271 name: nginx-deployment 272 spec: 273 replicas: 3`, 274 update: ` 275 apiVersion: apps/v1 276 kind: Deployment 277 metadata: 278 name: nginx-deployment 279 spec: 280 replicas: 4`, 281 local: ` 282 apiVersion: apps/v1 283 kind: Deployment 284 metadata: # kpt-merge: default/nginx-deployment 285 name: nginx-deployment-1 286 namespace: my-space 287 spec: 288 replicas: 3 289 --- 290 apiVersion: apps/v1 291 kind: Deployment 292 metadata: # kpt-merge: default/nginx-deployment 293 name: nginx-deployment-2 294 namespace: my-space 295 spec: 296 replicas: 3 297 `, 298 errMsg: `found duplicate "local" resources in file "f1.yaml"`}, 299 300 `Publisher changes name in upstream but want to maintain original identity, no local customizations, fetch upstream changes`: { 301 origin: ` 302 apiVersion: apps/v1 303 kind: Deployment 304 metadata: 305 name: nginx-deployment 306 spec: 307 replicas: 3`, 308 update: ` 309 apiVersion: apps/v1 310 kind: Deployment 311 metadata: # kpt-merge: /nginx-deployment 312 name: nginx-deployment-new 313 spec: 314 replicas: 4`, 315 local: ` 316 apiVersion: apps/v1 317 kind: Deployment 318 metadata: 319 name: nginx-deployment 320 spec: 321 replicas: 3 322 `, 323 expected: ` 324 apiVersion: apps/v1 325 kind: Deployment 326 metadata: # kpt-merge: /nginx-deployment 327 name: nginx-deployment-new 328 spec: 329 replicas: 4 330 `}, 331 332 `Publisher changes name in upstream but want to maintain original identity, consumer adds name-prefix on local, fetch upstream changes`: { 333 origin: ` 334 apiVersion: apps/v1 335 kind: Deployment 336 metadata: 337 name: nginx-deployment 338 spec: 339 replicas: 3`, 340 update: ` 341 apiVersion: apps/v1 342 kind: Deployment 343 metadata: # kpt-merge: default/nginx-deployment 344 name: nginx-deployment-new 345 spec: 346 replicas: 4`, 347 local: ` 348 apiVersion: apps/v1 349 kind: Deployment 350 metadata: # kpt-merge: default/nginx-deployment 351 name: dev-nginx-deployment 352 namespace: my-space 353 spec: 354 replicas: 3 355 `, 356 expected: ` 357 apiVersion: apps/v1 358 kind: Deployment 359 metadata: # kpt-merge: default/nginx-deployment 360 name: nginx-deployment-new 361 namespace: my-space 362 spec: 363 replicas: 4 364 `}, 365 366 `Publisher changes name in upstream but don't want to maintain original identity which is equivalent 367 to delete existing resource and add new one, consumer adds name-prefix on local`: { 368 origin: ` 369 apiVersion: apps/v1 370 kind: Deployment 371 metadata: 372 name: nginx-deployment 373 spec: 374 replicas: 3`, 375 update: ` 376 apiVersion: apps/v1 377 kind: Deployment 378 metadata: 379 name: nginx-deployment-new 380 spec: 381 replicas: 4`, 382 local: ` 383 apiVersion: apps/v1 384 kind: Deployment 385 metadata: # kpt-merge: /nginx-deployment 386 name: dev-nginx-deployment 387 namespace: my-space 388 spec: 389 replicas: 3 390 `, 391 expected: ` 392 apiVersion: apps/v1 393 kind: Deployment 394 metadata: # kpt-merge: /nginx-deployment 395 name: dev-nginx-deployment 396 namespace: my-space 397 spec: 398 replicas: 3 399 --- 400 apiVersion: apps/v1 401 kind: Deployment 402 metadata: 403 name: nginx-deployment-new 404 spec: 405 replicas: 4 406 `}, 407 408 `Publisher changes name multiple times in upstream but maintains original identity, no local customizations, 409 fetch upstream changes`: { 410 origin: ` 411 apiVersion: apps/v1 412 kind: Deployment 413 metadata: # kpt-merge: default/nginx-deployment 414 name: nginx-deployment-new 415 spec: 416 replicas: 4`, 417 update: ` 418 apiVersion: apps/v1 419 kind: Deployment 420 metadata: # kpt-merge: default/nginx-deployment 421 name: nginx-deployment-new-again 422 spec: 423 replicas: 5`, 424 local: ` 425 apiVersion: apps/v1 426 kind: Deployment 427 metadata: # kpt-merge: default/nginx-deployment 428 name: nginx-deployment-new 429 spec: 430 replicas: 5 431 `, 432 expected: ` 433 apiVersion: apps/v1 434 kind: Deployment 435 metadata: # kpt-merge: default/nginx-deployment 436 name: nginx-deployment-new-again 437 spec: 438 replicas: 5 439 `}, 440 441 `Publisher changes name multiple times in upstream but maintains original identity, consumer adds name-prefix 442 on local, fetch upstream changes`: { 443 origin: ` 444 apiVersion: apps/v1 445 kind: Deployment 446 metadata: # kpt-merge: default/nginx-deployment 447 name: nginx-deployment-new 448 spec: 449 replicas: 4`, 450 update: ` 451 apiVersion: apps/v1 452 kind: Deployment 453 metadata: # kpt-merge: default/nginx-deployment 454 name: nginx-deployment-new-again 455 spec: 456 replicas: 5`, 457 local: ` 458 apiVersion: apps/v1 459 kind: Deployment 460 metadata: # kpt-merge: default/nginx-deployment 461 name: dev-nginx-deployment 462 namespace: my-space 463 spec: 464 replicas: 5 465 `, 466 expected: ` 467 apiVersion: apps/v1 468 kind: Deployment 469 metadata: # kpt-merge: default/nginx-deployment 470 name: nginx-deployment-new-again 471 namespace: my-space 472 spec: 473 replicas: 5 474 `}, 475 `Publisher adds metadata.annotations in upstream in a non-identity kustomization resource, consumer adds changes to resource body 476 on local, fetch upstream changes`: { 477 origin: ` 478 apiVersion: kustomize.config.k8s.io/v1beta1 479 metadata: 480 labels: 481 color: blue 482 commonLabels: 483 app: dev`, 484 update: ` 485 apiVersion: kustomize.config.k8s.io/v1beta1 486 kind: Kustomization 487 metadata: 488 labels: 489 color: blue 490 annotations: 491 id.example.org: abcd 492 commonLabels: 493 app: dev`, 494 local: ` 495 apiVersion: kustomize.config.k8s.io/v1beta1 496 kind: Kustomization 497 metadata: 498 labels: 499 color: blue 500 commonLabels: 501 app: dev 502 tier: backend 503 `, 504 expected: ` 505 apiVersion: kustomize.config.k8s.io/v1beta1 506 kind: Kustomization 507 metadata: 508 labels: 509 color: blue 510 annotations: 511 id.example.org: abcd 512 commonLabels: 513 app: dev 514 tier: backend 515 `}, 516 `Publisher adds commonLabels in upstream in a non-identity kustomization resource, consumer adds changes to resource body 517 on local, fetch upstream changes`: { 518 origin: ` 519 commonLabels: 520 app: dev`, 521 update: ` 522 commonLabels: 523 tier: backend 524 app: dev`, 525 local: ` 526 commonLabels: 527 app: dev 528 tier: backend 529 db: mysql 530 `, 531 expected: ` 532 commonLabels: 533 app: dev 534 tier: backend 535 db: mysql 536 `}, 537 538 `Version changes are just like any other changes`: { 539 origin: ` 540 apiVersion: apps/v1 541 kind: Deployment 542 metadata: 543 name: nginx-deployment 544 spec: 545 replicas: 3`, 546 update: ` 547 apiVersion: apps/v2 548 kind: Deployment 549 metadata: 550 name: nginx-deployment 551 spec: 552 replicas: 4`, 553 local: ` 554 apiVersion: apps/v1 555 kind: Deployment 556 metadata: 557 name: nginx-deployment 558 spec: 559 replicas: 3 560 `, 561 expected: ` 562 apiVersion: apps/v2 563 kind: Deployment 564 metadata: 565 name: nginx-deployment 566 spec: 567 replicas: 4 568 `}, 569 } 570 571 for tn, tc := range testCases { 572 t.Run(tn, func(t *testing.T) { 573 // setup the local directory 574 dir := t.TempDir() 575 576 err := os.MkdirAll(filepath.Join(dir, "localDir"), 0700) 577 if !assert.NoError(t, err) { 578 t.FailNow() 579 } 580 581 err = os.MkdirAll(filepath.Join(dir, "updatedDir"), 0700) 582 if !assert.NoError(t, err) { 583 t.FailNow() 584 } 585 586 err = os.MkdirAll(filepath.Join(dir, "originalDir"), 0700) 587 if !assert.NoError(t, err) { 588 t.FailNow() 589 } 590 591 err = os.WriteFile(filepath.Join(dir, "originalDir", "f1.yaml"), []byte(strings.TrimSpace(tc.origin)), 0700) 592 if !assert.NoError(t, err) { 593 t.FailNow() 594 } 595 596 err = os.WriteFile(filepath.Join(dir, "updatedDir", "f1.yaml"), []byte(strings.TrimSpace(tc.update)), 0700) 597 if !assert.NoError(t, err) { 598 t.FailNow() 599 } 600 601 err = os.WriteFile(filepath.Join(dir, "localDir", "f1.yaml"), []byte(strings.TrimSpace(tc.local)), 0700) 602 if !assert.NoError(t, err) { 603 t.FailNow() 604 } 605 606 err = merge.Merge3{ 607 OriginalPath: filepath.Join(dir, "originalDir"), 608 UpdatedPath: filepath.Join(dir, "updatedDir"), 609 DestPath: filepath.Join(dir, "localDir"), 610 MergeOnPath: true, 611 }.Merge() 612 if tc.errMsg == "" { 613 if !assert.NoError(t, err) { 614 t.FailNow() 615 } 616 } else { 617 if !assert.Error(t, err) { 618 t.FailNow() 619 } 620 if !assert.Contains(t, err.Error(), tc.errMsg) { 621 t.FailNow() 622 } 623 return 624 } 625 626 b, err := os.ReadFile(filepath.Join(dir, "localDir", "f1.yaml")) 627 if !assert.NoError(t, err) { 628 t.FailNow() 629 } 630 if !assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(string(b))) { 631 t.FailNow() 632 } 633 }) 634 } 635 }