github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/repo/index_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 repo 18 19 import ( 20 "bufio" 21 "bytes" 22 "io/ioutil" 23 "net/http" 24 "os" 25 "path/filepath" 26 "sort" 27 "strings" 28 "testing" 29 30 "github.com/stefanmcshane/helm/pkg/chart" 31 "github.com/stefanmcshane/helm/pkg/cli" 32 "github.com/stefanmcshane/helm/pkg/getter" 33 "github.com/stefanmcshane/helm/pkg/helmpath" 34 ) 35 36 const ( 37 testfile = "testdata/local-index.yaml" 38 annotationstestfile = "testdata/local-index-annotations.yaml" 39 chartmuseumtestfile = "testdata/chartmuseum-index.yaml" 40 unorderedTestfile = "testdata/local-index-unordered.yaml" 41 testRepo = "test-repo" 42 indexWithDuplicates = ` 43 apiVersion: v1 44 entries: 45 nginx: 46 - urls: 47 - https://charts.helm.sh/stable/nginx-0.2.0.tgz 48 name: nginx 49 description: string 50 version: 0.2.0 51 home: https://github.com/something/else 52 digest: "sha256:1234567890abcdef" 53 nginx: 54 - urls: 55 - https://charts.helm.sh/stable/alpine-1.0.0.tgz 56 - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz 57 name: alpine 58 description: string 59 version: 1.0.0 60 home: https://github.com/something 61 digest: "sha256:1234567890abcdef" 62 ` 63 ) 64 65 func TestIndexFile(t *testing.T) { 66 i := NewIndexFile() 67 for _, x := range []struct { 68 md *chart.Metadata 69 filename string 70 baseURL string 71 digest string 72 }{ 73 {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, 74 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc"}, 75 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc"}, 76 {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"}, 77 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"}, 78 {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"}, 79 } { 80 if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { 81 t.Errorf("unexpected error adding to index: %s", err) 82 } 83 } 84 85 i.SortEntries() 86 87 if i.APIVersion != APIVersionV1 { 88 t.Error("Expected API version v1") 89 } 90 91 if len(i.Entries) != 3 { 92 t.Errorf("Expected 3 charts. Got %d", len(i.Entries)) 93 } 94 95 if i.Entries["clipper"][0].Name != "clipper" { 96 t.Errorf("Expected clipper, got %s", i.Entries["clipper"][0].Name) 97 } 98 99 if len(i.Entries["cutter"]) != 3 { 100 t.Error("Expected three cutters.") 101 } 102 103 // Test that the sort worked. 0.2 should be at the first index for Cutter. 104 if v := i.Entries["cutter"][0].Version; v != "0.2.0" { 105 t.Errorf("Unexpected first version: %s", v) 106 } 107 108 cv, err := i.Get("setter", "0.1.9") 109 if err == nil && !strings.Contains(cv.Metadata.Version, "0.1.9") { 110 t.Errorf("Unexpected version: %s", cv.Metadata.Version) 111 } 112 113 cv, err = i.Get("setter", "0.1.9+alpha") 114 if err != nil || cv.Metadata.Version != "0.1.9+alpha" { 115 t.Errorf("Expected version: 0.1.9+alpha") 116 } 117 } 118 119 func TestLoadIndex(t *testing.T) { 120 121 tests := []struct { 122 Name string 123 Filename string 124 }{ 125 { 126 Name: "regular index file", 127 Filename: testfile, 128 }, 129 { 130 Name: "chartmuseum index file", 131 Filename: chartmuseumtestfile, 132 }, 133 } 134 135 for _, tc := range tests { 136 tc := tc 137 t.Run(tc.Name, func(t *testing.T) { 138 t.Parallel() 139 i, err := LoadIndexFile(tc.Filename) 140 if err != nil { 141 t.Fatal(err) 142 } 143 verifyLocalIndex(t, i) 144 }) 145 } 146 } 147 148 // TestLoadIndex_Duplicates is a regression to make sure that we don't non-deterministically allow duplicate packages. 149 func TestLoadIndex_Duplicates(t *testing.T) { 150 if _, err := loadIndex([]byte(indexWithDuplicates), "indexWithDuplicates"); err == nil { 151 t.Errorf("Expected an error when duplicate entries are present") 152 } 153 } 154 155 func TestLoadIndex_Empty(t *testing.T) { 156 if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil { 157 t.Errorf("Expected an error when index.yaml is empty.") 158 } 159 } 160 161 func TestLoadIndexFileAnnotations(t *testing.T) { 162 i, err := LoadIndexFile(annotationstestfile) 163 if err != nil { 164 t.Fatal(err) 165 } 166 verifyLocalIndex(t, i) 167 168 if len(i.Annotations) != 1 { 169 t.Fatalf("Expected 1 annotation but got %d", len(i.Annotations)) 170 } 171 if i.Annotations["helm.sh/test"] != "foo bar" { 172 t.Error("Did not get expected value for helm.sh/test annotation") 173 } 174 } 175 176 func TestLoadUnorderedIndex(t *testing.T) { 177 i, err := LoadIndexFile(unorderedTestfile) 178 if err != nil { 179 t.Fatal(err) 180 } 181 verifyLocalIndex(t, i) 182 } 183 184 func TestMerge(t *testing.T) { 185 ind1 := NewIndexFile() 186 187 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 { 188 t.Fatalf("unexpected error: %s", err) 189 } 190 191 ind2 := NewIndexFile() 192 193 for _, x := range []struct { 194 md *chart.Metadata 195 filename string 196 baseURL string 197 digest string 198 }{ 199 {&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.2.0"}, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb"}, 200 {&chart.Metadata{APIVersion: "v2", Name: "doughnut", Version: "0.2.0"}, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb"}, 201 } { 202 if err := ind2.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { 203 t.Errorf("unexpected error: %s", err) 204 } 205 } 206 207 ind1.Merge(ind2) 208 209 if len(ind1.Entries) != 2 { 210 t.Errorf("Expected 2 entries, got %d", len(ind1.Entries)) 211 } 212 213 vs := ind1.Entries["dreadnought"] 214 if len(vs) != 2 { 215 t.Errorf("Expected 2 versions, got %d", len(vs)) 216 } 217 218 if v := vs[1]; v.Version != "0.2.0" { 219 t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version) 220 } 221 222 } 223 224 func TestDownloadIndexFile(t *testing.T) { 225 t.Run("should download index file", func(t *testing.T) { 226 srv, err := startLocalServerForTests(nil) 227 if err != nil { 228 t.Fatal(err) 229 } 230 defer srv.Close() 231 232 r, err := NewChartRepository(&Entry{ 233 Name: testRepo, 234 URL: srv.URL, 235 }, getter.All(&cli.EnvSettings{})) 236 if err != nil { 237 t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) 238 } 239 240 idx, err := r.DownloadIndexFile() 241 if err != nil { 242 t.Fatalf("Failed to download index file to %s: %#v", idx, err) 243 } 244 245 if _, err := os.Stat(idx); err != nil { 246 t.Fatalf("error finding created index file: %#v", err) 247 } 248 249 i, err := LoadIndexFile(idx) 250 if err != nil { 251 t.Fatalf("Index %q failed to parse: %s", testfile, err) 252 } 253 verifyLocalIndex(t, i) 254 255 // Check that charts file is also created 256 idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) 257 if _, err := os.Stat(idx); err != nil { 258 t.Fatalf("error finding created charts file: %#v", err) 259 } 260 261 b, err := ioutil.ReadFile(idx) 262 if err != nil { 263 t.Fatalf("error reading charts file: %#v", err) 264 } 265 verifyLocalChartsFile(t, b, i) 266 }) 267 268 t.Run("should not decode the path in the repo url while downloading index", func(t *testing.T) { 269 chartRepoURLPath := "/some%2Fpath/test" 270 fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") 271 if err != nil { 272 t.Fatal(err) 273 } 274 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 275 if r.URL.RawPath == chartRepoURLPath+"/index.yaml" { 276 w.Write(fileBytes) 277 } 278 }) 279 srv, err := startLocalServerForTests(handler) 280 if err != nil { 281 t.Fatal(err) 282 } 283 defer srv.Close() 284 285 r, err := NewChartRepository(&Entry{ 286 Name: testRepo, 287 URL: srv.URL + chartRepoURLPath, 288 }, getter.All(&cli.EnvSettings{})) 289 if err != nil { 290 t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) 291 } 292 293 idx, err := r.DownloadIndexFile() 294 if err != nil { 295 t.Fatalf("Failed to download index file to %s: %#v", idx, err) 296 } 297 298 if _, err := os.Stat(idx); err != nil { 299 t.Fatalf("error finding created index file: %#v", err) 300 } 301 302 i, err := LoadIndexFile(idx) 303 if err != nil { 304 t.Fatalf("Index %q failed to parse: %s", testfile, err) 305 } 306 verifyLocalIndex(t, i) 307 308 // Check that charts file is also created 309 idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) 310 if _, err := os.Stat(idx); err != nil { 311 t.Fatalf("error finding created charts file: %#v", err) 312 } 313 314 b, err := ioutil.ReadFile(idx) 315 if err != nil { 316 t.Fatalf("error reading charts file: %#v", err) 317 } 318 verifyLocalChartsFile(t, b, i) 319 }) 320 } 321 322 func verifyLocalIndex(t *testing.T, i *IndexFile) { 323 numEntries := len(i.Entries) 324 if numEntries != 3 { 325 t.Errorf("Expected 3 entries in index file but got %d", numEntries) 326 } 327 328 alpine, ok := i.Entries["alpine"] 329 if !ok { 330 t.Fatalf("'alpine' section not found.") 331 } 332 333 if l := len(alpine); l != 1 { 334 t.Fatalf("'alpine' should have 1 chart, got %d", l) 335 } 336 337 nginx, ok := i.Entries["nginx"] 338 if !ok || len(nginx) != 2 { 339 t.Fatalf("Expected 2 nginx entries") 340 } 341 342 expects := []*ChartVersion{ 343 { 344 Metadata: &chart.Metadata{ 345 APIVersion: "v2", 346 Name: "alpine", 347 Description: "string", 348 Version: "1.0.0", 349 Keywords: []string{"linux", "alpine", "small", "sumtin"}, 350 Home: "https://github.com/something", 351 }, 352 URLs: []string{ 353 "https://charts.helm.sh/stable/alpine-1.0.0.tgz", 354 "http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz", 355 }, 356 Digest: "sha256:1234567890abcdef", 357 }, 358 { 359 Metadata: &chart.Metadata{ 360 APIVersion: "v2", 361 Name: "nginx", 362 Description: "string", 363 Version: "0.2.0", 364 Keywords: []string{"popular", "web server", "proxy"}, 365 Home: "https://github.com/something/else", 366 }, 367 URLs: []string{ 368 "https://charts.helm.sh/stable/nginx-0.2.0.tgz", 369 }, 370 Digest: "sha256:1234567890abcdef", 371 }, 372 { 373 Metadata: &chart.Metadata{ 374 APIVersion: "v2", 375 Name: "nginx", 376 Description: "string", 377 Version: "0.1.0", 378 Keywords: []string{"popular", "web server", "proxy"}, 379 Home: "https://github.com/something", 380 }, 381 URLs: []string{ 382 "https://charts.helm.sh/stable/nginx-0.1.0.tgz", 383 }, 384 Digest: "sha256:1234567890abcdef", 385 }, 386 } 387 tests := []*ChartVersion{alpine[0], nginx[0], nginx[1]} 388 389 for i, tt := range tests { 390 expect := expects[i] 391 if tt.Name != expect.Name { 392 t.Errorf("Expected name %q, got %q", expect.Name, tt.Name) 393 } 394 if tt.Description != expect.Description { 395 t.Errorf("Expected description %q, got %q", expect.Description, tt.Description) 396 } 397 if tt.Version != expect.Version { 398 t.Errorf("Expected version %q, got %q", expect.Version, tt.Version) 399 } 400 if tt.Digest != expect.Digest { 401 t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest) 402 } 403 if tt.Home != expect.Home { 404 t.Errorf("Expected home %q, got %q", expect.Home, tt.Home) 405 } 406 407 for i, url := range tt.URLs { 408 if url != expect.URLs[i] { 409 t.Errorf("Expected URL %q, got %q", expect.URLs[i], url) 410 } 411 } 412 for i, kw := range tt.Keywords { 413 if kw != expect.Keywords[i] { 414 t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw) 415 } 416 } 417 } 418 } 419 420 func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) { 421 var expected, real []string 422 for chart := range indexContent.Entries { 423 expected = append(expected, chart) 424 } 425 sort.Strings(expected) 426 427 scanner := bufio.NewScanner(bytes.NewReader(chartsContent)) 428 for scanner.Scan() { 429 real = append(real, scanner.Text()) 430 } 431 sort.Strings(real) 432 433 if strings.Join(expected, " ") != strings.Join(real, " ") { 434 t.Errorf("Cached charts file content unexpected. Expected:\n%s\ngot:\n%s", expected, real) 435 } 436 } 437 438 func TestIndexDirectory(t *testing.T) { 439 dir := "testdata/repository" 440 index, err := IndexDirectory(dir, "http://localhost:8080") 441 if err != nil { 442 t.Fatal(err) 443 } 444 445 if l := len(index.Entries); l != 3 { 446 t.Fatalf("Expected 3 entries, got %d", l) 447 } 448 449 // Other things test the entry generation more thoroughly. We just test a 450 // few fields. 451 452 corpus := []struct{ chartName, downloadLink string }{ 453 {"frobnitz", "http://localhost:8080/frobnitz-1.2.3.tgz"}, 454 {"zarthal", "http://localhost:8080/universe/zarthal-1.0.0.tgz"}, 455 } 456 457 for _, test := range corpus { 458 cname := test.chartName 459 frobs, ok := index.Entries[cname] 460 if !ok { 461 t.Fatalf("Could not read chart %s", cname) 462 } 463 464 frob := frobs[0] 465 if frob.Digest == "" { 466 t.Errorf("Missing digest of file %s.", frob.Name) 467 } 468 if frob.URLs[0] != test.downloadLink { 469 t.Errorf("Unexpected URLs: %v", frob.URLs) 470 } 471 if frob.Name != cname { 472 t.Errorf("Expected %q, got %q", cname, frob.Name) 473 } 474 } 475 } 476 477 func TestIndexAdd(t *testing.T) { 478 i := NewIndexFile() 479 480 for _, x := range []struct { 481 md *chart.Metadata 482 filename string 483 baseURL string 484 digest string 485 }{ 486 487 {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, 488 {&chart.Metadata{APIVersion: "v2", Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, 489 {&chart.Metadata{APIVersion: "v2", Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890"}, 490 } { 491 if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { 492 t.Errorf("unexpected error adding to index: %s", err) 493 } 494 } 495 496 if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" { 497 t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0]) 498 } 499 if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" { 500 t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0]) 501 } 502 if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" { 503 t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) 504 } 505 506 // test error condition 507 if err := i.MustAdd(&chart.Metadata{}, "error-0.1.0.tgz", "", ""); err == nil { 508 t.Fatal("expected error adding to index") 509 } 510 } 511 512 func TestIndexWrite(t *testing.T) { 513 i := NewIndexFile() 514 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 { 515 t.Fatalf("unexpected error: %s", err) 516 } 517 dir := t.TempDir() 518 testpath := filepath.Join(dir, "test") 519 i.WriteFile(testpath, 0600) 520 521 got, err := ioutil.ReadFile(testpath) 522 if err != nil { 523 t.Fatal(err) 524 } 525 if !strings.Contains(string(got), "clipper-0.1.0.tgz") { 526 t.Fatal("Index files doesn't contain expected content") 527 } 528 }