github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/fnruntime/runner_test.go (about) 1 // Copyright 2019 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 pipeline provides struct definitions for Pipeline and utility 16 // methods to read and write a pipeline resource. 17 package fnruntime 18 19 import ( 20 "bytes" 21 "context" 22 "os" 23 "path" 24 "strings" 25 "testing" 26 27 "github.com/GoogleContainerTools/kpt/internal/printer" 28 "github.com/GoogleContainerTools/kpt/internal/types" 29 kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 30 "github.com/stretchr/testify/assert" 31 "sigs.k8s.io/kustomize/kyaml/filesys" 32 "sigs.k8s.io/kustomize/kyaml/fn/framework" 33 "sigs.k8s.io/kustomize/kyaml/kio" 34 "sigs.k8s.io/kustomize/kyaml/yaml" 35 ) 36 37 func TestFunctionConfig(t *testing.T) { 38 type input struct { 39 name string 40 fn kptfilev1.Function 41 configFileContent string 42 expected string 43 } 44 45 cases := []input{ 46 { 47 name: "no config", 48 fn: kptfilev1.Function{}, 49 expected: "", 50 }, 51 { 52 name: "file config", 53 fn: kptfilev1.Function{}, 54 configFileContent: `apiVersion: cft.dev/v1alpha1 55 kind: ResourceHierarchy 56 metadata: 57 name: root-hierarchy 58 namespace: hierarchy`, 59 expected: `apiVersion: cft.dev/v1alpha1 60 kind: ResourceHierarchy 61 metadata: 62 name: root-hierarchy 63 namespace: hierarchy 64 `, 65 }, 66 { 67 name: "map config", 68 fn: kptfilev1.Function{ 69 ConfigMap: map[string]string{ 70 "foo": "bar", 71 }, 72 }, 73 expected: `apiVersion: v1 74 kind: ConfigMap 75 metadata: 76 name: function-input 77 data: {foo: bar} 78 `, 79 }, 80 } 81 82 for _, c := range cases { 83 c := c 84 t.Run(c.name, func(t *testing.T) { 85 if c.configFileContent != "" { 86 tmp, err := os.CreateTemp("", "kpt-pipeline-*") 87 assert.NoError(t, err, "unexpected error") 88 _, err = tmp.WriteString(c.configFileContent) 89 assert.NoError(t, err, "unexpected error") 90 c.fn.ConfigPath = path.Base(tmp.Name()) 91 } 92 fsys := filesys.MakeFsOnDisk() 93 cn, err := newFnConfig(fsys, &c.fn, types.UniquePath(os.TempDir())) 94 assert.NoError(t, err, "unexpected error") 95 actual, err := cn.String() 96 assert.NoError(t, err, "unexpected error") 97 assert.Equal(t, c.expected, actual, "unexpected result") 98 }) 99 } 100 } 101 102 func TestMultilineFormatter(t *testing.T) { 103 type testcase struct { 104 ml *multiLineFormatter 105 expected string 106 } 107 108 testcases := map[string]testcase{ 109 "multiline should format lines and truncate": { 110 ml: &multiLineFormatter{ 111 Title: "Results", 112 Lines: []string{ 113 "line-1", 114 "line-2", 115 "line-3", 116 "line-4", 117 "line-5", 118 }, 119 MaxLines: 3, 120 TruncateOutput: true, 121 }, 122 expected: ` Results: 123 line-1 124 line-2 125 line-3 126 ...(2 line(s) truncated, use '--truncate-output=false' to disable) 127 `, 128 }, 129 "multiline should format without truncate": { 130 ml: &multiLineFormatter{ 131 Title: "Results", 132 Lines: []string{ 133 "line-1", 134 "line-2", 135 "line-3", 136 "line-4", 137 "line-5", 138 }, 139 }, 140 expected: ` Results: 141 line-1 142 line-2 143 line-3 144 line-4 145 line-5 146 `, 147 }, 148 } 149 for name, c := range testcases { 150 c := c 151 t.Run(name, func(t *testing.T) { 152 assert.Equal(t, c.expected, c.ml.String()) 153 }) 154 } 155 } 156 157 func TestEnforcePathInvariants(t *testing.T) { 158 tests := map[string]struct { 159 input string // input 160 expectedErr string // expected result 161 }{ 162 "duplicate": { 163 input: `apiVersion: v1 164 kind: Custom 165 metadata: 166 name: a 167 annotations: 168 config.kubernetes.io/path: 'my/path/custom.yaml' 169 config.kubernetes.io/index: '0' 170 --- 171 apiVersion: v1 172 kind: Custom 173 metadata: 174 name: b 175 annotations: 176 config.kubernetes.io/path: 'my/path/custom.yaml' 177 config.kubernetes.io/index: '0' 178 `, 179 expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`, 180 }, 181 "duplicate with `./` prefix": { 182 input: `apiVersion: v1 183 kind: Custom 184 metadata: 185 name: a 186 annotations: 187 config.kubernetes.io/path: 'my/path/custom.yaml' 188 config.kubernetes.io/index: '0' 189 --- 190 apiVersion: v1 191 kind: Custom 192 metadata: 193 name: b 194 annotations: 195 config.kubernetes.io/path: './my/path/custom.yaml' 196 config.kubernetes.io/index: '0' 197 `, 198 expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`, 199 }, 200 "duplicate path, not index": { 201 input: `apiVersion: v1 202 kind: Custom 203 metadata: 204 name: a 205 annotations: 206 config.kubernetes.io/path: 'my/path/custom.yaml' 207 config.kubernetes.io/index: '0' 208 --- 209 apiVersion: v1 210 kind: Custom 211 metadata: 212 name: b 213 annotations: 214 config.kubernetes.io/path: 'my/path/custom.yaml' 215 config.kubernetes.io/index: '1' 216 `, 217 }, 218 "duplicate index, not path": { 219 input: `apiVersion: v1 220 kind: Custom 221 metadata: 222 name: a 223 annotations: 224 config.kubernetes.io/path: 'my/path/a.yaml' 225 config.kubernetes.io/index: '0' 226 --- 227 apiVersion: v1 228 kind: Custom 229 metadata: 230 name: b 231 annotations: 232 config.kubernetes.io/path: 'my/path/b.yaml' 233 config.kubernetes.io/index: '0' 234 `, 235 }, 236 "larger number of resources with duplicate": { 237 input: `apiVersion: v1 238 kind: Custom 239 metadata: 240 name: a 241 annotations: 242 config.kubernetes.io/path: 'my/path/a.yaml' 243 config.kubernetes.io/index: '0' 244 --- 245 apiVersion: v1 246 kind: Custom 247 metadata: 248 name: b 249 annotations: 250 config.kubernetes.io/path: 'my/path/a.yaml' 251 config.kubernetes.io/index: '1' 252 --- 253 apiVersion: v1 254 kind: Custom 255 metadata: 256 name: b 257 annotations: 258 config.kubernetes.io/path: 'my/path/b.yaml' 259 config.kubernetes.io/index: '0' 260 --- 261 apiVersion: v1 262 kind: Custom 263 metadata: 264 name: b 265 annotations: 266 config.kubernetes.io/path: 'my/path/b.yaml' 267 config.kubernetes.io/index: '1' 268 --- 269 apiVersion: v1 270 kind: Custom 271 metadata: 272 name: b 273 annotations: 274 config.kubernetes.io/path: 'my/path/b.yaml' 275 config.kubernetes.io/index: '2' 276 --- 277 apiVersion: v1 278 kind: Custom 279 metadata: 280 name: b 281 annotations: 282 config.kubernetes.io/path: 'my/path/c.yaml' 283 config.kubernetes.io/index: '0' 284 --- 285 apiVersion: v1 286 kind: Custom 287 metadata: 288 name: b 289 annotations: 290 config.kubernetes.io/path: 'my/path/c.yaml' 291 config.kubernetes.io/index: '1' 292 --- 293 apiVersion: v1 294 kind: Custom 295 metadata: 296 name: b 297 annotations: 298 config.kubernetes.io/path: 'my/path/b.yaml' 299 config.kubernetes.io/index: '1' 300 `, 301 expectedErr: `resource at path "my/path/b.yaml" and index "1" already exists`, 302 }, 303 "larger number of resources without duplicates": { 304 input: `apiVersion: v1 305 kind: Custom 306 metadata: 307 name: a 308 annotations: 309 config.kubernetes.io/path: 'my/path/a.yaml' 310 config.kubernetes.io/index: '0' 311 --- 312 apiVersion: v1 313 kind: Custom 314 metadata: 315 name: b 316 annotations: 317 config.kubernetes.io/path: 'my/path/a.yaml' 318 config.kubernetes.io/index: '1' 319 --- 320 apiVersion: v1 321 kind: Custom 322 metadata: 323 name: b 324 annotations: 325 config.kubernetes.io/path: 'my/path/b.yaml' 326 config.kubernetes.io/index: '0' 327 --- 328 apiVersion: v1 329 kind: Custom 330 metadata: 331 name: b 332 annotations: 333 config.kubernetes.io/path: 'my/path/b.yaml' 334 config.kubernetes.io/index: '1' 335 --- 336 apiVersion: v1 337 kind: Custom 338 metadata: 339 name: b 340 annotations: 341 config.kubernetes.io/path: 'my/path/b.yaml' 342 config.kubernetes.io/index: '2' 343 --- 344 apiVersion: v1 345 kind: Custom 346 metadata: 347 name: b 348 annotations: 349 config.kubernetes.io/path: 'my/path/c.yaml' 350 config.kubernetes.io/index: '0' 351 --- 352 apiVersion: v1 353 kind: Custom 354 metadata: 355 name: b 356 annotations: 357 config.kubernetes.io/path: 'my/path/c.yaml' 358 config.kubernetes.io/index: '1' 359 --- 360 apiVersion: v1 361 kind: Custom 362 metadata: 363 name: b 364 annotations: 365 config.kubernetes.io/path: 'my/path/b.yaml' 366 config.kubernetes.io/index: '3' 367 `, 368 }, 369 370 "no error": { 371 input: ` 372 apiVersion: apps/v1 373 kind: StatefulSet 374 metadata: 375 name: my-stateful-set 376 annotations: 377 config.kubernetes.io/path: my-stateful-set.yaml 378 spec: 379 replicas: 3 380 `, 381 }, 382 "with ../ prefix": { 383 input: ` 384 apiVersion: apps/v1 385 kind: StatefulSet 386 metadata: 387 name: my-stateful-set 388 annotations: 389 config.kubernetes.io/path: ../my-stateful-set.yaml 390 spec: 391 replicas: 3 392 393 `, 394 expectedErr: "function must not modify resources outside of package: resource has path ../my-stateful-set.yaml", 395 }, 396 "with nested ../ in path": { 397 input: ` 398 apiVersion: apps/v1 399 kind: StatefulSet 400 metadata: 401 name: my-stateful-set 402 annotations: 403 config.kubernetes.io/path: a/b/../../../my-stateful-set.yaml 404 spec: 405 replicas: 3 406 `, 407 expectedErr: "function must not modify resources outside of package: resource has path a/b/../../../my-stateful-set.yaml", 408 }, 409 } 410 for _, tc := range tests { 411 out := &bytes.Buffer{} 412 r := kio.ByteReadWriter{ 413 Reader: bytes.NewBufferString(tc.input), 414 Writer: out, 415 KeepReaderAnnotations: true, 416 OmitReaderAnnotations: true, 417 WrapBareSeqNode: true, 418 } 419 n, err := r.Read() 420 if err != nil { 421 t.FailNow() 422 } 423 err = enforcePathInvariants(n) 424 if err != nil && tc.expectedErr == "" { 425 t.Errorf("unexpected error %s", err.Error()) 426 t.FailNow() 427 } 428 if tc.expectedErr != "" && err == nil { 429 t.Errorf("expected error %s", tc.expectedErr) 430 t.FailNow() 431 } 432 if tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr) { 433 t.Errorf("wanted error %s, got %s", tc.expectedErr, err.Error()) 434 t.FailNow() 435 } 436 } 437 } 438 439 func TestGetResourceRefMetadata(t *testing.T) { 440 tests := map[string]struct { 441 input string // input 442 expected string // expected result 443 }{ 444 "new format with name": { 445 input: ` 446 message: selector is required 447 severity: error 448 resourceRef: 449 apiVersion: apps/v1 450 kind: Deployment 451 name: nginx-deployment 452 field: 453 path: selector 454 file: 455 path: resources.yaml 456 `, 457 expected: `message: selector is required 458 severity: error 459 resourceRef: 460 apiVersion: apps/v1 461 kind: Deployment 462 name: nginx-deployment 463 field: 464 path: selector 465 file: 466 path: resources.yaml 467 `, 468 }, 469 "new format with namespace": { 470 input: ` 471 message: selector is required 472 severity: error 473 resourceRef: 474 apiVersion: apps/v1 475 kind: Deployment 476 name: nginx-deployment 477 namespace: my-namespace 478 field: 479 path: selector 480 file: 481 path: resources.yaml 482 `, 483 expected: `message: selector is required 484 severity: error 485 resourceRef: 486 apiVersion: apps/v1 487 kind: Deployment 488 name: nginx-deployment 489 namespace: my-namespace 490 field: 491 path: selector 492 file: 493 path: resources.yaml 494 `, 495 }, 496 "old format with name": { 497 input: ` 498 message: selector is required 499 severity: error 500 resourceRef: 501 apiVersion: apps/v1 502 kind: Deployment 503 metadata: 504 name: nginx-deployment 505 field: 506 path: selector 507 file: 508 path: resources.yaml 509 `, 510 expected: `message: selector is required 511 severity: error 512 resourceRef: 513 apiVersion: apps/v1 514 kind: Deployment 515 name: nginx-deployment 516 field: 517 path: selector 518 file: 519 path: resources.yaml 520 `, 521 }, 522 "old format with namespace": { 523 input: ` 524 message: selector is required 525 severity: error 526 resourceRef: 527 apiVersion: apps/v1 528 kind: Deployment 529 metadata: 530 name: nginx-deployment 531 namespace: my-namespace 532 field: 533 path: selector 534 file: 535 path: resources.yaml 536 `, 537 expected: `message: selector is required 538 severity: error 539 resourceRef: 540 apiVersion: apps/v1 541 kind: Deployment 542 name: nginx-deployment 543 namespace: my-namespace 544 field: 545 path: selector 546 file: 547 path: resources.yaml 548 `, 549 }, 550 "no resourceRef": { 551 input: ` 552 message: selector is required 553 severity: error 554 field: 555 path: selector 556 file: 557 path: resources.yaml 558 `, 559 expected: `message: selector is required 560 severity: error 561 field: 562 path: selector 563 file: 564 path: resources.yaml 565 `, 566 }, 567 } 568 for _, tc := range tests { 569 yml, err := yaml.Parse(tc.input) 570 assert.NoError(t, err) 571 572 result := &framework.Result{} 573 err = yaml.Unmarshal([]byte(tc.input), result) 574 assert.NoError(t, err) 575 assert.NoError(t, populateResourceRef(yml, result)) 576 577 out, err := yaml.Marshal(result) 578 assert.NoError(t, err) 579 assert.Equal(t, tc.expected, string(out)) 580 } 581 } 582 583 func TestPrintFnStderr(t *testing.T) { 584 tests := map[string]struct { 585 input string // input 586 truncateOutput bool // whether to truncate output 587 expected string // expected result 588 }{ 589 "no output": { 590 input: ``, 591 truncateOutput: true, 592 expected: ``, 593 }, 594 "truncated output": { 595 input: `0 596 1 597 2 598 3 599 4 600 5`, 601 truncateOutput: true, 602 expected: ` Stderr: 603 "0" 604 "1" 605 "2" 606 "3" 607 ...(2 line(s) truncated, use '--truncate-output=false' to disable) 608 `, 609 }, 610 "non-truncated output": { 611 input: `0 612 1 613 2 614 3 615 4 616 5`, 617 truncateOutput: false, 618 expected: ` Stderr: 619 "0" 620 "1" 621 "2" 622 "3" 623 "4" 624 "5" 625 `, 626 }, 627 } 628 cleanupFunc := func() func() { 629 origTruncateOutput := printer.TruncateOutput 630 return func() { 631 printer.TruncateOutput = origTruncateOutput 632 } 633 }() 634 defer cleanupFunc() 635 for testName, tc := range tests { 636 t.Run(testName, func(t *testing.T) { 637 printer.TruncateOutput = tc.truncateOutput 638 out := &bytes.Buffer{} 639 err := &bytes.Buffer{} 640 ctx := printer.WithContext(context.Background(), printer.New(out, err)) 641 642 printFnStderr(ctx, tc.input) 643 644 assert.Equal(t, tc.expected, err.String()) 645 assert.Equal(t, "", out.String()) 646 }) 647 } 648 }