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  }