1
16
17 package repo
18
19 import (
20 "bufio"
21 "bytes"
22 "encoding/json"
23 "fmt"
24 "net/http"
25 "os"
26 "path/filepath"
27 "sort"
28 "strings"
29 "testing"
30
31 "helm.sh/helm/v3/pkg/chart"
32 "helm.sh/helm/v3/pkg/cli"
33 "helm.sh/helm/v3/pkg/getter"
34 "helm.sh/helm/v3/pkg/helmpath"
35 )
36
37 const (
38 testfile = "testdata/local-index.yaml"
39 annotationstestfile = "testdata/local-index-annotations.yaml"
40 chartmuseumtestfile = "testdata/chartmuseum-index.yaml"
41 unorderedTestfile = "testdata/local-index-unordered.yaml"
42 jsonTestfile = "testdata/local-index.json"
43 testRepo = "test-repo"
44 indexWithDuplicates = `
45 apiVersion: v1
46 entries:
47 nginx:
48 - urls:
49 - https://charts.helm.sh/stable/nginx-0.2.0.tgz
50 name: nginx
51 description: string
52 version: 0.2.0
53 home: https://github.com/something/else
54 digest: "sha256:1234567890abcdef"
55 nginx:
56 - urls:
57 - https://charts.helm.sh/stable/alpine-1.0.0.tgz
58 - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
59 name: alpine
60 description: string
61 version: 1.0.0
62 home: https://github.com/something
63 digest: "sha256:1234567890abcdef"
64 `
65 indexWithEmptyEntry = `
66 apiVersion: v1
67 entries:
68 grafana:
69 - apiVersion: v2
70 name: grafana
71 foo:
72 -
73 bar:
74 - digest: "sha256:1234567890abcdef"
75 urls:
76 - https://charts.helm.sh/stable/alpine-1.0.0.tgz
77 `
78 )
79
80 func TestIndexFile(t *testing.T) {
81 i := NewIndexFile()
82 for _, x := range []struct {
83 md *chart.Metadata
84 filename string
85 baseURL string
86 digest string
87 }{
88 {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
89 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc"},
90 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc"},
91 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"},
92 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"},
93 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
94 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8"}, "setter-0.1.8.tgz", "http://example.com/charts", "sha256:1234567890abc"},
95 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8+beta"}, "setter-0.1.8+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
96 } {
97 if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
98 t.Errorf("unexpected error adding to index: %s", err)
99 }
100 }
101
102 i.SortEntries()
103
104 if i.APIVersion != APIVersionV1 {
105 t.Error("Expected API version v1")
106 }
107
108 if len(i.Entries) != 3 {
109 t.Errorf("Expected 3 charts. Got %d", len(i.Entries))
110 }
111
112 if i.Entries["clipper"][0].Name != "clipper" {
113 t.Errorf("Expected clipper, got %s", i.Entries["clipper"][0].Name)
114 }
115
116 if len(i.Entries["cutter"]) != 3 {
117 t.Error("Expected three cutters.")
118 }
119
120
121 if v := i.Entries["cutter"][0].Version; v != "0.2.0" {
122 t.Errorf("Unexpected first version: %s", v)
123 }
124
125 cv, err := i.Get("setter", "0.1.9")
126 if err == nil && !strings.Contains(cv.Metadata.Version, "0.1.9") {
127 t.Errorf("Unexpected version: %s", cv.Metadata.Version)
128 }
129
130 cv, err = i.Get("setter", "0.1.9+alpha")
131 if err != nil || cv.Metadata.Version != "0.1.9+alpha" {
132 t.Errorf("Expected version: 0.1.9+alpha")
133 }
134
135 cv, err = i.Get("setter", "0.1.8")
136 if err != nil || cv.Metadata.Version != "0.1.8" {
137 t.Errorf("Expected version: 0.1.8")
138 }
139 }
140
141 func TestLoadIndex(t *testing.T) {
142
143 tests := []struct {
144 Name string
145 Filename string
146 }{
147 {
148 Name: "regular index file",
149 Filename: testfile,
150 },
151 {
152 Name: "chartmuseum index file",
153 Filename: chartmuseumtestfile,
154 },
155 {
156 Name: "JSON index file",
157 Filename: jsonTestfile,
158 },
159 }
160
161 for _, tc := range tests {
162 tc := tc
163 t.Run(tc.Name, func(t *testing.T) {
164 t.Parallel()
165 i, err := LoadIndexFile(tc.Filename)
166 if err != nil {
167 t.Fatal(err)
168 }
169 verifyLocalIndex(t, i)
170 })
171 }
172 }
173
174
175 func TestLoadIndex_Duplicates(t *testing.T) {
176 if _, err := loadIndex([]byte(indexWithDuplicates), "indexWithDuplicates"); err == nil {
177 t.Errorf("Expected an error when duplicate entries are present")
178 }
179 }
180
181 func TestLoadIndex_EmptyEntry(t *testing.T) {
182 if _, err := loadIndex([]byte(indexWithEmptyEntry), "indexWithEmptyEntry"); err != nil {
183 t.Errorf("unexpected error: %s", err)
184 }
185 }
186
187 func TestLoadIndex_Empty(t *testing.T) {
188 if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil {
189 t.Errorf("Expected an error when index.yaml is empty.")
190 }
191 }
192
193 func TestLoadIndexFileAnnotations(t *testing.T) {
194 i, err := LoadIndexFile(annotationstestfile)
195 if err != nil {
196 t.Fatal(err)
197 }
198 verifyLocalIndex(t, i)
199
200 if len(i.Annotations) != 1 {
201 t.Fatalf("Expected 1 annotation but got %d", len(i.Annotations))
202 }
203 if i.Annotations["helm.sh/test"] != "foo bar" {
204 t.Error("Did not get expected value for helm.sh/test annotation")
205 }
206 }
207
208 func TestLoadUnorderedIndex(t *testing.T) {
209 i, err := LoadIndexFile(unorderedTestfile)
210 if err != nil {
211 t.Fatal(err)
212 }
213 verifyLocalIndex(t, i)
214 }
215
216 func TestMerge(t *testing.T) {
217 ind1 := NewIndexFile()
218
219 if err := ind1.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.1.0"}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa"); err != nil {
220 t.Fatalf("unexpected error: %s", err)
221 }
222
223 ind2 := NewIndexFile()
224
225 for _, x := range []struct {
226 md *chart.Metadata
227 filename string
228 baseURL string
229 digest string
230 }{
231 {&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.2.0"}, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb"},
232 {&chart.Metadata{APIVersion: "v2", Name: "doughnut", Version: "0.2.0"}, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb"},
233 } {
234 if err := ind2.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
235 t.Errorf("unexpected error: %s", err)
236 }
237 }
238
239 ind1.Merge(ind2)
240
241 if len(ind1.Entries) != 2 {
242 t.Errorf("Expected 2 entries, got %d", len(ind1.Entries))
243 }
244
245 vs := ind1.Entries["dreadnought"]
246 if len(vs) != 2 {
247 t.Errorf("Expected 2 versions, got %d", len(vs))
248 }
249
250 if v := vs[1]; v.Version != "0.2.0" {
251 t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version)
252 }
253
254 }
255
256 func TestDownloadIndexFile(t *testing.T) {
257 t.Run("should download index file", func(t *testing.T) {
258 srv, err := startLocalServerForTests(nil)
259 if err != nil {
260 t.Fatal(err)
261 }
262 defer srv.Close()
263
264 r, err := NewChartRepository(&Entry{
265 Name: testRepo,
266 URL: srv.URL,
267 }, getter.All(&cli.EnvSettings{}))
268 if err != nil {
269 t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
270 }
271
272 idx, err := r.DownloadIndexFile()
273 if err != nil {
274 t.Fatalf("Failed to download index file to %s: %#v", idx, err)
275 }
276
277 if _, err := os.Stat(idx); err != nil {
278 t.Fatalf("error finding created index file: %#v", err)
279 }
280
281 i, err := LoadIndexFile(idx)
282 if err != nil {
283 t.Fatalf("Index %q failed to parse: %s", testfile, err)
284 }
285 verifyLocalIndex(t, i)
286
287
288 idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
289 if _, err := os.Stat(idx); err != nil {
290 t.Fatalf("error finding created charts file: %#v", err)
291 }
292
293 b, err := os.ReadFile(idx)
294 if err != nil {
295 t.Fatalf("error reading charts file: %#v", err)
296 }
297 verifyLocalChartsFile(t, b, i)
298 })
299
300 t.Run("should not decode the path in the repo url while downloading index", func(t *testing.T) {
301 chartRepoURLPath := "/some%2Fpath/test"
302 fileBytes, err := os.ReadFile("testdata/local-index.yaml")
303 if err != nil {
304 t.Fatal(err)
305 }
306 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
307 if r.URL.RawPath == chartRepoURLPath+"/index.yaml" {
308 w.Write(fileBytes)
309 }
310 })
311 srv, err := startLocalServerForTests(handler)
312 if err != nil {
313 t.Fatal(err)
314 }
315 defer srv.Close()
316
317 r, err := NewChartRepository(&Entry{
318 Name: testRepo,
319 URL: srv.URL + chartRepoURLPath,
320 }, getter.All(&cli.EnvSettings{}))
321 if err != nil {
322 t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
323 }
324
325 idx, err := r.DownloadIndexFile()
326 if err != nil {
327 t.Fatalf("Failed to download index file to %s: %#v", idx, err)
328 }
329
330 if _, err := os.Stat(idx); err != nil {
331 t.Fatalf("error finding created index file: %#v", err)
332 }
333
334 i, err := LoadIndexFile(idx)
335 if err != nil {
336 t.Fatalf("Index %q failed to parse: %s", testfile, err)
337 }
338 verifyLocalIndex(t, i)
339
340
341 idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
342 if _, err := os.Stat(idx); err != nil {
343 t.Fatalf("error finding created charts file: %#v", err)
344 }
345
346 b, err := os.ReadFile(idx)
347 if err != nil {
348 t.Fatalf("error reading charts file: %#v", err)
349 }
350 verifyLocalChartsFile(t, b, i)
351 })
352 }
353
354 func verifyLocalIndex(t *testing.T, i *IndexFile) {
355 numEntries := len(i.Entries)
356 if numEntries != 3 {
357 t.Errorf("Expected 3 entries in index file but got %d", numEntries)
358 }
359
360 alpine, ok := i.Entries["alpine"]
361 if !ok {
362 t.Fatalf("'alpine' section not found.")
363 }
364
365 if l := len(alpine); l != 1 {
366 t.Fatalf("'alpine' should have 1 chart, got %d", l)
367 }
368
369 nginx, ok := i.Entries["nginx"]
370 if !ok || len(nginx) != 2 {
371 t.Fatalf("Expected 2 nginx entries")
372 }
373
374 expects := []*ChartVersion{
375 {
376 Metadata: &chart.Metadata{
377 APIVersion: "v2",
378 Name: "alpine",
379 Description: "string",
380 Version: "1.0.0",
381 Keywords: []string{"linux", "alpine", "small", "sumtin"},
382 Home: "https://github.com/something",
383 },
384 URLs: []string{
385 "https://charts.helm.sh/stable/alpine-1.0.0.tgz",
386 "http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz",
387 },
388 Digest: "sha256:1234567890abcdef",
389 },
390 {
391 Metadata: &chart.Metadata{
392 APIVersion: "v2",
393 Name: "nginx",
394 Description: "string",
395 Version: "0.2.0",
396 Keywords: []string{"popular", "web server", "proxy"},
397 Home: "https://github.com/something/else",
398 },
399 URLs: []string{
400 "https://charts.helm.sh/stable/nginx-0.2.0.tgz",
401 },
402 Digest: "sha256:1234567890abcdef",
403 },
404 {
405 Metadata: &chart.Metadata{
406 APIVersion: "v2",
407 Name: "nginx",
408 Description: "string",
409 Version: "0.1.0",
410 Keywords: []string{"popular", "web server", "proxy"},
411 Home: "https://github.com/something",
412 },
413 URLs: []string{
414 "https://charts.helm.sh/stable/nginx-0.1.0.tgz",
415 },
416 Digest: "sha256:1234567890abcdef",
417 },
418 }
419 tests := []*ChartVersion{alpine[0], nginx[0], nginx[1]}
420
421 for i, tt := range tests {
422 expect := expects[i]
423 if tt.Name != expect.Name {
424 t.Errorf("Expected name %q, got %q", expect.Name, tt.Name)
425 }
426 if tt.Description != expect.Description {
427 t.Errorf("Expected description %q, got %q", expect.Description, tt.Description)
428 }
429 if tt.Version != expect.Version {
430 t.Errorf("Expected version %q, got %q", expect.Version, tt.Version)
431 }
432 if tt.Digest != expect.Digest {
433 t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest)
434 }
435 if tt.Home != expect.Home {
436 t.Errorf("Expected home %q, got %q", expect.Home, tt.Home)
437 }
438
439 for i, url := range tt.URLs {
440 if url != expect.URLs[i] {
441 t.Errorf("Expected URL %q, got %q", expect.URLs[i], url)
442 }
443 }
444 for i, kw := range tt.Keywords {
445 if kw != expect.Keywords[i] {
446 t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw)
447 }
448 }
449 }
450 }
451
452 func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) {
453 var expected, reald []string
454 for chart := range indexContent.Entries {
455 expected = append(expected, chart)
456 }
457 sort.Strings(expected)
458
459 scanner := bufio.NewScanner(bytes.NewReader(chartsContent))
460 for scanner.Scan() {
461 reald = append(reald, scanner.Text())
462 }
463 sort.Strings(reald)
464
465 if strings.Join(expected, " ") != strings.Join(reald, " ") {
466 t.Errorf("Cached charts file content unexpected. Expected:\n%s\ngot:\n%s", expected, reald)
467 }
468 }
469
470 func TestIndexDirectory(t *testing.T) {
471 dir := "testdata/repository"
472 index, err := IndexDirectory(dir, "http://localhost:8080")
473 if err != nil {
474 t.Fatal(err)
475 }
476
477 if l := len(index.Entries); l != 3 {
478 t.Fatalf("Expected 3 entries, got %d", l)
479 }
480
481
482
483
484 corpus := []struct{ chartName, downloadLink string }{
485 {"frobnitz", "http://localhost:8080/frobnitz-1.2.3.tgz"},
486 {"zarthal", "http://localhost:8080/universe/zarthal-1.0.0.tgz"},
487 }
488
489 for _, test := range corpus {
490 cname := test.chartName
491 frobs, ok := index.Entries[cname]
492 if !ok {
493 t.Fatalf("Could not read chart %s", cname)
494 }
495
496 frob := frobs[0]
497 if frob.Digest == "" {
498 t.Errorf("Missing digest of file %s.", frob.Name)
499 }
500 if frob.URLs[0] != test.downloadLink {
501 t.Errorf("Unexpected URLs: %v", frob.URLs)
502 }
503 if frob.Name != cname {
504 t.Errorf("Expected %q, got %q", cname, frob.Name)
505 }
506 }
507 }
508
509 func TestIndexAdd(t *testing.T) {
510 i := NewIndexFile()
511
512 for _, x := range []struct {
513 md *chart.Metadata
514 filename string
515 baseURL string
516 digest string
517 }{
518
519 {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
520 {&chart.Metadata{APIVersion: "v2", Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"},
521 {&chart.Metadata{APIVersion: "v2", Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890"},
522 } {
523 if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil {
524 t.Errorf("unexpected error adding to index: %s", err)
525 }
526 }
527
528 if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" {
529 t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0])
530 }
531 if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" {
532 t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0])
533 }
534 if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" {
535 t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0])
536 }
537
538
539 if err := i.MustAdd(&chart.Metadata{}, "error-0.1.0.tgz", "", ""); err == nil {
540 t.Fatal("expected error adding to index")
541 }
542 }
543
544 func TestIndexWrite(t *testing.T) {
545 i := NewIndexFile()
546 if err := i.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"); err != nil {
547 t.Fatalf("unexpected error: %s", err)
548 }
549 dir := t.TempDir()
550 testpath := filepath.Join(dir, "test")
551 i.WriteFile(testpath, 0600)
552
553 got, err := os.ReadFile(testpath)
554 if err != nil {
555 t.Fatal(err)
556 }
557 if !strings.Contains(string(got), "clipper-0.1.0.tgz") {
558 t.Fatal("Index files doesn't contain expected content")
559 }
560 }
561
562 func TestIndexJSONWrite(t *testing.T) {
563 i := NewIndexFile()
564 if err := i.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"); err != nil {
565 t.Fatalf("unexpected error: %s", err)
566 }
567 dir := t.TempDir()
568 testpath := filepath.Join(dir, "test")
569 i.WriteJSONFile(testpath, 0600)
570
571 got, err := os.ReadFile(testpath)
572 if err != nil {
573 t.Fatal(err)
574 }
575 if !json.Valid(got) {
576 t.Fatal("Index files doesn't contain valid JSON")
577 }
578 if !strings.Contains(string(got), "clipper-0.1.0.tgz") {
579 t.Fatal("Index files doesn't contain expected content")
580 }
581 }
582
583 func TestAddFileIndexEntriesNil(t *testing.T) {
584 i := NewIndexFile()
585 i.APIVersion = chart.APIVersionV1
586 i.Entries = nil
587 for _, x := range []struct {
588 md *chart.Metadata
589 filename string
590 baseURL string
591 digest string
592 }{
593 {&chart.Metadata{APIVersion: "v2", Name: " ", Version: "8033-5.apinie+s.r"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"},
594 } {
595 if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err == nil {
596 t.Errorf("expected err to be non-nil when entries not initialized")
597 }
598 }
599 }
600
601 func TestIgnoreSkippableChartValidationError(t *testing.T) {
602 type TestCase struct {
603 Input error
604 ErrorSkipped bool
605 }
606 testCases := map[string]TestCase{
607 "nil": {
608 Input: nil,
609 },
610 "generic_error": {
611 Input: fmt.Errorf("foo"),
612 },
613 "non_skipped_validation_error": {
614 Input: chart.ValidationError("chart.metadata.type must be application or library"),
615 },
616 "skipped_validation_error": {
617 Input: chart.ValidationErrorf("more than one dependency with name or alias %q", "foo"),
618 ErrorSkipped: true,
619 },
620 }
621
622 for name, tc := range testCases {
623 t.Run(name, func(t *testing.T) {
624 result := ignoreSkippableChartValidationError(tc.Input)
625
626 if tc.Input == nil {
627 if result != nil {
628 t.Error("expected nil result for nil input")
629 }
630 return
631 }
632
633 if tc.ErrorSkipped {
634 if result != nil {
635 t.Error("expected nil result for skipped error")
636 }
637 return
638 }
639
640 if tc.Input != result {
641 t.Error("expected the result equal to input")
642 }
643
644 })
645 }
646 }
647
View as plain text