github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/chart/loader/load_test.go (about) 1 /* 2 Copyright The Helm 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 loader 18 19 import ( 20 "archive/tar" 21 "bytes" 22 "compress/gzip" 23 "io" 24 "io/ioutil" 25 "log" 26 "os" 27 "path/filepath" 28 "runtime" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/stefanmcshane/helm/pkg/chart" 34 ) 35 36 func TestLoadDir(t *testing.T) { 37 l, err := Loader("testdata/frobnitz") 38 if err != nil { 39 t.Fatalf("Failed to load testdata: %s", err) 40 } 41 c, err := l.Load() 42 if err != nil { 43 t.Fatalf("Failed to load testdata: %s", err) 44 } 45 verifyFrobnitz(t, c) 46 verifyChart(t, c) 47 verifyDependencies(t, c) 48 verifyDependenciesLock(t, c) 49 } 50 51 func TestLoadDirWithDevNull(t *testing.T) { 52 if runtime.GOOS == "windows" { 53 t.Skip("test only works on unix systems with /dev/null present") 54 } 55 56 l, err := Loader("testdata/frobnitz_with_dev_null") 57 if err != nil { 58 t.Fatalf("Failed to load testdata: %s", err) 59 } 60 if _, err := l.Load(); err == nil { 61 t.Errorf("packages with an irregular file (/dev/null) should not load") 62 } 63 } 64 65 func TestLoadDirWithSymlink(t *testing.T) { 66 sym := filepath.Join("..", "LICENSE") 67 link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE") 68 69 if err := os.Symlink(sym, link); err != nil { 70 t.Fatal(err) 71 } 72 73 defer os.Remove(link) 74 75 l, err := Loader("testdata/frobnitz_with_symlink") 76 if err != nil { 77 t.Fatalf("Failed to load testdata: %s", err) 78 } 79 80 c, err := l.Load() 81 if err != nil { 82 t.Fatalf("Failed to load testdata: %s", err) 83 } 84 verifyFrobnitz(t, c) 85 verifyChart(t, c) 86 verifyDependencies(t, c) 87 verifyDependenciesLock(t, c) 88 } 89 90 func TestBomTestData(t *testing.T) { 91 testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} 92 for _, file := range testFiles { 93 data, err := ioutil.ReadFile("testdata/" + file) 94 if err != nil || !bytes.HasPrefix(data, utf8bom) { 95 t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) 96 } 97 } 98 99 archive, err := ioutil.ReadFile("testdata/frobnitz_with_bom.tgz") 100 if err != nil { 101 t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) 102 } 103 unzipped, err := gzip.NewReader(bytes.NewReader(archive)) 104 if err != nil { 105 t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) 106 } 107 defer unzipped.Close() 108 for _, testFile := range testFiles { 109 data := make([]byte, 3) 110 err := unzipped.Reset(bytes.NewReader(archive)) 111 if err != nil { 112 t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) 113 } 114 tr := tar.NewReader(unzipped) 115 for { 116 file, err := tr.Next() 117 if err == io.EOF { 118 break 119 } 120 if err != nil { 121 t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) 122 } 123 if file != nil && strings.EqualFold(file.Name, testFile) { 124 _, err := tr.Read(data) 125 if err != nil { 126 t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) 127 } else { 128 break 129 } 130 } 131 } 132 if !bytes.Equal(data, utf8bom) { 133 t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) 134 } 135 } 136 } 137 138 func TestLoadDirWithUTFBOM(t *testing.T) { 139 l, err := Loader("testdata/frobnitz_with_bom") 140 if err != nil { 141 t.Fatalf("Failed to load testdata: %s", err) 142 } 143 c, err := l.Load() 144 if err != nil { 145 t.Fatalf("Failed to load testdata: %s", err) 146 } 147 verifyFrobnitz(t, c) 148 verifyChart(t, c) 149 verifyDependencies(t, c) 150 verifyDependenciesLock(t, c) 151 verifyBomStripped(t, c.Files) 152 } 153 154 func TestLoadArchiveWithUTFBOM(t *testing.T) { 155 l, err := Loader("testdata/frobnitz_with_bom.tgz") 156 if err != nil { 157 t.Fatalf("Failed to load testdata: %s", err) 158 } 159 c, err := l.Load() 160 if err != nil { 161 t.Fatalf("Failed to load testdata: %s", err) 162 } 163 verifyFrobnitz(t, c) 164 verifyChart(t, c) 165 verifyDependencies(t, c) 166 verifyDependenciesLock(t, c) 167 verifyBomStripped(t, c.Files) 168 } 169 170 func TestLoadV1(t *testing.T) { 171 l, err := Loader("testdata/frobnitz.v1") 172 if err != nil { 173 t.Fatalf("Failed to load testdata: %s", err) 174 } 175 c, err := l.Load() 176 if err != nil { 177 t.Fatalf("Failed to load testdata: %s", err) 178 } 179 verifyDependencies(t, c) 180 verifyDependenciesLock(t, c) 181 } 182 183 func TestLoadFileV1(t *testing.T) { 184 l, err := Loader("testdata/frobnitz.v1.tgz") 185 if err != nil { 186 t.Fatalf("Failed to load testdata: %s", err) 187 } 188 c, err := l.Load() 189 if err != nil { 190 t.Fatalf("Failed to load testdata: %s", err) 191 } 192 verifyDependencies(t, c) 193 verifyDependenciesLock(t, c) 194 } 195 196 func TestLoadFile(t *testing.T) { 197 l, err := Loader("testdata/frobnitz-1.2.3.tgz") 198 if err != nil { 199 t.Fatalf("Failed to load testdata: %s", err) 200 } 201 c, err := l.Load() 202 if err != nil { 203 t.Fatalf("Failed to load testdata: %s", err) 204 } 205 verifyFrobnitz(t, c) 206 verifyChart(t, c) 207 verifyDependencies(t, c) 208 } 209 210 func TestLoadFiles_BadCases(t *testing.T) { 211 for _, tt := range []struct { 212 name string 213 bufferedFiles []*BufferedFile 214 expectError string 215 }{ 216 { 217 name: "These files contain only requirements.lock", 218 bufferedFiles: []*BufferedFile{ 219 { 220 Name: "requirements.lock", 221 Data: []byte(""), 222 }, 223 }, 224 expectError: "validation: chart.metadata.apiVersion is required"}, 225 } { 226 _, err := LoadFiles(tt.bufferedFiles) 227 if err == nil { 228 t.Fatal("expected error when load illegal files") 229 } 230 if !strings.Contains(err.Error(), tt.expectError) { 231 t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.name) 232 } 233 } 234 } 235 236 func TestLoadFiles(t *testing.T) { 237 goodFiles := []*BufferedFile{ 238 { 239 Name: "Chart.yaml", 240 Data: []byte(`apiVersion: v1 241 name: frobnitz 242 description: This is a frobnitz. 243 version: "1.2.3" 244 keywords: 245 - frobnitz 246 - sprocket 247 - dodad 248 maintainers: 249 - name: The Helm Team 250 email: helm@example.com 251 - name: Someone Else 252 email: nobody@example.com 253 sources: 254 - https://example.com/foo/bar 255 home: http://example.com 256 icon: https://example.com/64x64.png 257 `), 258 }, 259 { 260 Name: "values.yaml", 261 Data: []byte("var: some values"), 262 }, 263 { 264 Name: "values.schema.json", 265 Data: []byte("type: Values"), 266 }, 267 { 268 Name: "templates/deployment.yaml", 269 Data: []byte("some deployment"), 270 }, 271 { 272 Name: "templates/service.yaml", 273 Data: []byte("some service"), 274 }, 275 } 276 277 c, err := LoadFiles(goodFiles) 278 if err != nil { 279 t.Errorf("Expected good files to be loaded, got %v", err) 280 } 281 282 if c.Name() != "frobnitz" { 283 t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name()) 284 } 285 286 if c.Values["var"] != "some values" { 287 t.Error("Expected chart values to be populated with default values") 288 } 289 290 if len(c.Raw) != 5 { 291 t.Errorf("Expected %d files, got %d", 5, len(c.Raw)) 292 } 293 294 if !bytes.Equal(c.Schema, []byte("type: Values")) { 295 t.Error("Expected chart schema to be populated with default values") 296 } 297 298 if len(c.Templates) != 2 { 299 t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) 300 } 301 302 if _, err = LoadFiles([]*BufferedFile{}); err == nil { 303 t.Fatal("Expected err to be non-nil") 304 } 305 if err.Error() != "Chart.yaml file is missing" { 306 t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) 307 } 308 } 309 310 // Test the order of file loading. The Chart.yaml file needs to come first for 311 // later comparison checks. See https://github.com/helm/helm/pull/8948 312 func TestLoadFilesOrder(t *testing.T) { 313 goodFiles := []*BufferedFile{ 314 { 315 Name: "requirements.yaml", 316 Data: []byte("dependencies:"), 317 }, 318 { 319 Name: "values.yaml", 320 Data: []byte("var: some values"), 321 }, 322 323 { 324 Name: "templates/deployment.yaml", 325 Data: []byte("some deployment"), 326 }, 327 { 328 Name: "templates/service.yaml", 329 Data: []byte("some service"), 330 }, 331 { 332 Name: "Chart.yaml", 333 Data: []byte(`apiVersion: v1 334 name: frobnitz 335 description: This is a frobnitz. 336 version: "1.2.3" 337 keywords: 338 - frobnitz 339 - sprocket 340 - dodad 341 maintainers: 342 - name: The Helm Team 343 email: helm@example.com 344 - name: Someone Else 345 email: nobody@example.com 346 sources: 347 - https://example.com/foo/bar 348 home: http://example.com 349 icon: https://example.com/64x64.png 350 `), 351 }, 352 } 353 354 // Capture stderr to make sure message about Chart.yaml handle dependencies 355 // is not present 356 r, w, err := os.Pipe() 357 if err != nil { 358 t.Fatalf("Unable to create pipe: %s", err) 359 } 360 stderr := log.Writer() 361 log.SetOutput(w) 362 defer func() { 363 log.SetOutput(stderr) 364 }() 365 366 _, err = LoadFiles(goodFiles) 367 if err != nil { 368 t.Errorf("Expected good files to be loaded, got %v", err) 369 } 370 w.Close() 371 372 var text bytes.Buffer 373 io.Copy(&text, r) 374 if text.String() != "" { 375 t.Errorf("Expected no message to Stderr, got %s", text.String()) 376 } 377 378 } 379 380 // Packaging the chart on a Windows machine will produce an 381 // archive that has \\ as delimiters. Test that we support these archives 382 func TestLoadFileBackslash(t *testing.T) { 383 c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz") 384 if err != nil { 385 t.Fatalf("Failed to load testdata: %s", err) 386 } 387 verifyChartFileAndTemplate(t, c, "frobnitz_backslash") 388 verifyChart(t, c) 389 verifyDependencies(t, c) 390 } 391 392 func TestLoadV2WithReqs(t *testing.T) { 393 l, err := Loader("testdata/frobnitz.v2.reqs") 394 if err != nil { 395 t.Fatalf("Failed to load testdata: %s", err) 396 } 397 c, err := l.Load() 398 if err != nil { 399 t.Fatalf("Failed to load testdata: %s", err) 400 } 401 verifyDependencies(t, c) 402 verifyDependenciesLock(t, c) 403 } 404 405 func TestLoadInvalidArchive(t *testing.T) { 406 tmpdir := t.TempDir() 407 408 writeTar := func(filename, internalPath string, body []byte) { 409 dest, err := os.Create(filename) 410 if err != nil { 411 t.Fatal(err) 412 } 413 zipper := gzip.NewWriter(dest) 414 tw := tar.NewWriter(zipper) 415 416 h := &tar.Header{ 417 Name: internalPath, 418 Mode: 0755, 419 Size: int64(len(body)), 420 ModTime: time.Now(), 421 } 422 if err := tw.WriteHeader(h); err != nil { 423 t.Fatal(err) 424 } 425 if _, err := tw.Write(body); err != nil { 426 t.Fatal(err) 427 } 428 tw.Close() 429 zipper.Close() 430 dest.Close() 431 } 432 433 for _, tt := range []struct { 434 chartname string 435 internal string 436 expectError string 437 }{ 438 {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"}, 439 {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"}, 440 {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"}, 441 {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"}, 442 {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"}, 443 {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"}, 444 {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"}, 445 {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"}, 446 {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"}, 447 {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"}, 448 {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"}, 449 {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"}, 450 451 // Under special circumstances, this can get normalized to things that look like absolute Windows paths 452 {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"}, 453 {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"}, 454 {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"}, 455 } { 456 illegalChart := filepath.Join(tmpdir, tt.chartname) 457 writeTar(illegalChart, tt.internal, []byte("hello: world")) 458 _, err := Load(illegalChart) 459 if err == nil { 460 t.Fatal("expected error when unpacking illegal files") 461 } 462 if !strings.Contains(err.Error(), tt.expectError) { 463 t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname) 464 } 465 } 466 467 // Make sure that absolute path gets interpreted as relative 468 illegalChart := filepath.Join(tmpdir, "abs-path.tgz") 469 writeTar(illegalChart, "/Chart.yaml", []byte("hello: world")) 470 _, err := Load(illegalChart) 471 if err.Error() != "validation: chart.metadata.name is required" { 472 t.Error(err) 473 } 474 475 // And just to validate that the above was not spurious 476 illegalChart = filepath.Join(tmpdir, "abs-path2.tgz") 477 writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world")) 478 _, err = Load(illegalChart) 479 if err.Error() != "Chart.yaml file is missing" { 480 t.Errorf("Unexpected error message: %s", err) 481 } 482 483 // Finally, test that drive letter gets stripped off on Windows 484 illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz") 485 writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world")) 486 _, err = Load(illegalChart) 487 if err.Error() != "validation: chart.metadata.name is required" { 488 t.Error(err) 489 } 490 } 491 492 func verifyChart(t *testing.T, c *chart.Chart) { 493 t.Helper() 494 if c.Name() == "" { 495 t.Fatalf("No chart metadata found on %v", c) 496 } 497 t.Logf("Verifying chart %s", c.Name()) 498 if len(c.Templates) != 1 { 499 t.Errorf("Expected 1 template, got %d", len(c.Templates)) 500 } 501 502 numfiles := 6 503 if len(c.Files) != numfiles { 504 t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) 505 for _, n := range c.Files { 506 t.Logf("\t%s", n.Name) 507 } 508 } 509 510 if len(c.Dependencies()) != 2 { 511 t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies()) 512 for _, d := range c.Dependencies() { 513 t.Logf("\tSubchart: %s\n", d.Name()) 514 } 515 } 516 517 expect := map[string]map[string]string{ 518 "alpine": { 519 "version": "0.1.0", 520 }, 521 "mariner": { 522 "version": "4.3.2", 523 }, 524 } 525 526 for _, dep := range c.Dependencies() { 527 if dep.Metadata == nil { 528 t.Fatalf("expected metadata on dependency: %v", dep) 529 } 530 exp, ok := expect[dep.Name()] 531 if !ok { 532 t.Fatalf("Unknown dependency %s", dep.Name()) 533 } 534 if exp["version"] != dep.Metadata.Version { 535 t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) 536 } 537 } 538 539 } 540 541 func verifyDependencies(t *testing.T, c *chart.Chart) { 542 if len(c.Metadata.Dependencies) != 2 { 543 t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) 544 } 545 tests := []*chart.Dependency{ 546 {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, 547 {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, 548 } 549 for i, tt := range tests { 550 d := c.Metadata.Dependencies[i] 551 if d.Name != tt.Name { 552 t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) 553 } 554 if d.Version != tt.Version { 555 t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) 556 } 557 if d.Repository != tt.Repository { 558 t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) 559 } 560 } 561 } 562 563 func verifyDependenciesLock(t *testing.T, c *chart.Chart) { 564 if len(c.Metadata.Dependencies) != 2 { 565 t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) 566 } 567 tests := []*chart.Dependency{ 568 {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, 569 {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, 570 } 571 for i, tt := range tests { 572 d := c.Metadata.Dependencies[i] 573 if d.Name != tt.Name { 574 t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) 575 } 576 if d.Version != tt.Version { 577 t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) 578 } 579 if d.Repository != tt.Repository { 580 t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) 581 } 582 } 583 } 584 585 func verifyFrobnitz(t *testing.T, c *chart.Chart) { 586 verifyChartFileAndTemplate(t, c, "frobnitz") 587 } 588 589 func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { 590 if c.Metadata == nil { 591 t.Fatal("Metadata is nil") 592 } 593 if c.Name() != name { 594 t.Errorf("Expected %s, got %s", name, c.Name()) 595 } 596 if len(c.Templates) != 1 { 597 t.Fatalf("Expected 1 template, got %d", len(c.Templates)) 598 } 599 if c.Templates[0].Name != "templates/template.tpl" { 600 t.Errorf("Unexpected template: %s", c.Templates[0].Name) 601 } 602 if len(c.Templates[0].Data) == 0 { 603 t.Error("No template data.") 604 } 605 if len(c.Files) != 6 { 606 t.Fatalf("Expected 6 Files, got %d", len(c.Files)) 607 } 608 if len(c.Dependencies()) != 2 { 609 t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies())) 610 } 611 if len(c.Metadata.Dependencies) != 2 { 612 t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies)) 613 } 614 if len(c.Lock.Dependencies) != 2 { 615 t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies)) 616 } 617 618 for _, dep := range c.Dependencies() { 619 switch dep.Name() { 620 case "mariner": 621 case "alpine": 622 if len(dep.Templates) != 1 { 623 t.Fatalf("Expected 1 template, got %d", len(dep.Templates)) 624 } 625 if dep.Templates[0].Name != "templates/alpine-pod.yaml" { 626 t.Errorf("Unexpected template: %s", dep.Templates[0].Name) 627 } 628 if len(dep.Templates[0].Data) == 0 { 629 t.Error("No template data.") 630 } 631 if len(dep.Files) != 1 { 632 t.Fatalf("Expected 1 Files, got %d", len(dep.Files)) 633 } 634 if len(dep.Dependencies()) != 2 { 635 t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies())) 636 } 637 default: 638 t.Errorf("Unexpected dependency %s", dep.Name()) 639 } 640 } 641 } 642 643 func verifyBomStripped(t *testing.T, files []*chart.File) { 644 for _, file := range files { 645 if bytes.HasPrefix(file.Data, utf8bom) { 646 t.Errorf("Byte Order Mark still present in processed file %s", file.Name) 647 } 648 } 649 }