884 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			884 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2020 The Flux authors
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package repository
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	. "github.com/onsi/gomega"
 | |
| 	"github.com/opencontainers/go-digest"
 | |
| 	"helm.sh/helm/v3/pkg/chart"
 | |
| 	helmgetter "helm.sh/helm/v3/pkg/getter"
 | |
| 	"helm.sh/helm/v3/pkg/repo"
 | |
| 
 | |
| 	"github.com/fluxcd/source-controller/internal/helm"
 | |
| )
 | |
| 
 | |
| var now = time.Now()
 | |
| 
 | |
| const (
 | |
| 	testFile                = "../testdata/local-index.yaml"
 | |
| 	chartmuseumTestFile     = "../testdata/chartmuseum-index.yaml"
 | |
| 	chartmuseumJSONTestFile = "../testdata/chartmuseum-index.json"
 | |
| 	unorderedTestFile       = "../testdata/local-index-unordered.yaml"
 | |
| )
 | |
| 
 | |
| // mockGetter is a simple mocking getter.Getter implementation, returning
 | |
| // a byte response to any provided URL.
 | |
| type mockGetter struct {
 | |
| 	Response      []byte
 | |
| 	LastCalledURL string
 | |
| }
 | |
| 
 | |
| func (g *mockGetter) Get(u string, _ ...helmgetter.Option) (*bytes.Buffer, error) {
 | |
| 	r := g.Response
 | |
| 	g.LastCalledURL = u
 | |
| 	return bytes.NewBuffer(r), nil
 | |
| }
 | |
| 
 | |
| // Index load tests are derived from https://github.com/helm/helm/blob/v3.3.4/pkg/repo/index_test.go#L108
 | |
| // to ensure parity with Helm behaviour.
 | |
| func TestIndexFromFile(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	// Create an index file that exceeds the max index size.
 | |
| 	tmpDir := t.TempDir()
 | |
| 	bigIndexFile := filepath.Join(tmpDir, "index.yaml")
 | |
| 	data := make([]byte, helm.MaxIndexSize+10)
 | |
| 	g.Expect(os.WriteFile(bigIndexFile, data, 0o640)).ToNot(HaveOccurred())
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		filename string
 | |
| 		wantErr  string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "regular index file",
 | |
| 			filename: testFile,
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "chartmuseum index file",
 | |
| 			filename: chartmuseumTestFile,
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "chartmuseum json index file",
 | |
| 			filename: chartmuseumJSONTestFile,
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "error if index size exceeds max size",
 | |
| 			filename: bigIndexFile,
 | |
| 			wantErr:  "exceeds the maximum index file size",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		tt := tt
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			i, err := IndexFromFile(tt.filename)
 | |
| 			if tt.wantErr != "" {
 | |
| 				g.Expect(err).To(HaveOccurred())
 | |
| 				g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 
 | |
| 			verifyLocalIndex(t, i)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestIndexFromBytes(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name        string
 | |
| 		b           []byte
 | |
| 		wantName    string
 | |
| 		wantVersion string
 | |
| 		wantDigest  string
 | |
| 		wantErr     string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "index",
 | |
| 			b: []byte(`
 | |
| apiVersion: v1
 | |
| entries:
 | |
|   nginx:
 | |
|     - urls:
 | |
|         - https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz
 | |
|       name: nginx
 | |
|       description: string
 | |
|       version: 0.2.0
 | |
|       home: https://github.com/something/else
 | |
|       digest: "sha256:1234567890abcdef"
 | |
| `),
 | |
| 			wantName:    "nginx",
 | |
| 			wantVersion: "0.2.0",
 | |
| 			wantDigest:  "sha256:1234567890abcdef",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "index without API version",
 | |
| 			b: []byte(`entries:
 | |
|   nginx:
 | |
|     - name: nginx`),
 | |
| 			wantErr: "no API version specified",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "index with duplicate entry",
 | |
| 			b: []byte(`apiVersion: v1
 | |
| entries:
 | |
|   nginx:
 | |
|     - name: nginx"
 | |
|   nginx:
 | |
|     - name: nginx`),
 | |
| 			wantErr: "key \"nginx\" already set in map",
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		tt := tt
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			i, err := IndexFromBytes(tt.b)
 | |
| 			if tt.wantErr != "" {
 | |
| 				g.Expect(err).To(HaveOccurred())
 | |
| 				g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
 | |
| 				g.Expect(i).To(BeNil())
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 			g.Expect(i).ToNot(BeNil())
 | |
| 			got, err := i.Get(tt.wantName, tt.wantVersion)
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 			g.Expect(got.Digest).To(Equal(tt.wantDigest))
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestIndexFromBytes_Unordered(t *testing.T) {
 | |
| 	b, err := os.ReadFile(unorderedTestFile)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	i, err := IndexFromBytes(b)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	verifyLocalIndex(t, i)
 | |
| }
 | |
| 
 | |
| func TestNewChartRepository(t *testing.T) {
 | |
| 	repositoryURL := "https://example.com"
 | |
| 	providers := helmgetter.Providers{
 | |
| 		helmgetter.Provider{
 | |
| 			Schemes: []string{"https"},
 | |
| 			New:     helmgetter.NewHTTPGetter,
 | |
| 		},
 | |
| 	}
 | |
| 	options := []helmgetter.Option{helmgetter.WithBasicAuth("username", "password")}
 | |
| 
 | |
| 	t.Run("should construct chart repository", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r, err := NewChartRepository(repositoryURL, "", providers, nil, options...)
 | |
| 		g.Expect(err).ToNot(HaveOccurred())
 | |
| 		g.Expect(r).ToNot(BeNil())
 | |
| 		g.Expect(r.URL).To(Equal(repositoryURL))
 | |
| 		g.Expect(r.Client).ToNot(BeNil())
 | |
| 		g.Expect(r.Options).To(Equal(options))
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should error on URL parsing failure", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 		r, err := NewChartRepository("https://ex ample.com", "", nil, nil, nil)
 | |
| 		g.Expect(err).To(HaveOccurred())
 | |
| 		g.Expect(err).To(BeAssignableToTypeOf(&url.Error{}))
 | |
| 		g.Expect(r).To(BeNil())
 | |
| 
 | |
| 	})
 | |
| 
 | |
| 	t.Run("should error on unsupported scheme", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r, err := NewChartRepository("http://example.com", "", providers, nil, nil)
 | |
| 		g.Expect(err).To(HaveOccurred())
 | |
| 		g.Expect(err.Error()).To(Equal("scheme \"http\" not supported"))
 | |
| 		g.Expect(r).To(BeNil())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_GetChartVersion(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	r.Index = repo.NewIndexFile()
 | |
| 	charts := []struct {
 | |
| 		name    string
 | |
| 		version string
 | |
| 		url     string
 | |
| 		digest  string
 | |
| 		created time.Time
 | |
| 	}{
 | |
| 		{name: "chart", version: "0.0.1", url: "http://example.com/charts", digest: "sha256:1234567890"},
 | |
| 		{name: "chart", version: "0.1.0", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
 | |
| 		{name: "chart", version: "0.1.1", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
 | |
| 		{name: "chart", version: "0.1.5+b.min.minute", url: "http://example.com/charts", digest: "sha256:1234567890abc", created: now.Add(-time.Minute)},
 | |
| 		{name: "chart", version: "0.1.5+a.min.hour", url: "http://example.com/charts", digest: "sha256:1234567890abc", created: now.Add(-time.Hour)},
 | |
| 		{name: "chart", version: "0.1.5+c.now", url: "http://example.com/charts", digest: "sha256:1234567890abc", created: now},
 | |
| 		{name: "chart", version: "0.2.0", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
 | |
| 		{name: "chart", version: "1.0.0", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
 | |
| 		{name: "chart", version: "1.1.0-rc.1", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
 | |
| 	}
 | |
| 	for _, c := range charts {
 | |
| 		g.Expect(r.Index.MustAdd(
 | |
| 			&chart.Metadata{Name: c.name, Version: c.version},
 | |
| 			fmt.Sprintf("%s-%s.tgz", c.name, c.version), c.url, c.digest),
 | |
| 		).To(Succeed())
 | |
| 		if !c.created.IsZero() {
 | |
| 			r.Index.Entries["chart"][len(r.Index.Entries["chart"])-1].Created = c.created
 | |
| 		}
 | |
| 	}
 | |
| 	r.Index.SortEntries()
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name         string
 | |
| 		chartName    string
 | |
| 		chartVersion string
 | |
| 		wantVersion  string
 | |
| 		wantErr      string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:         "exact match",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: "0.0.1",
 | |
| 			wantVersion:  "0.0.1",
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "stable version",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: "",
 | |
| 			wantVersion:  "1.0.0",
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "stable version (asterisk)",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: "*",
 | |
| 			wantVersion:  "1.0.0",
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "semver range",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: "<1.0.0",
 | |
| 			wantVersion:  "0.2.0",
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "unfulfilled range",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: ">2.0.0",
 | |
| 			wantErr:      "no 'chart' chart with version matching '>2.0.0' found",
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "invalid chart",
 | |
| 			chartName: "non-existing",
 | |
| 			wantErr:   repo.ErrNoChartName.Error(),
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "match newest if ambiguous",
 | |
| 			chartName:    "chart",
 | |
| 			chartVersion: "0.1.5",
 | |
| 			wantVersion:  "0.1.5+c.now",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			cv, err := r.GetChartVersion(tt.chartName, tt.chartVersion)
 | |
| 			if tt.wantErr != "" {
 | |
| 				g.Expect(err).To(HaveOccurred())
 | |
| 				g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
 | |
| 				g.Expect(cv).To(BeNil())
 | |
| 				return
 | |
| 			}
 | |
| 			g.Expect(cv).ToNot(BeNil())
 | |
| 			g.Expect(cv.Metadata.Name).To(Equal(tt.chartName))
 | |
| 			g.Expect(cv.Metadata.Version).To(Equal(tt.wantVersion))
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestChartRepository_DownloadChart(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name         string
 | |
| 		url          string
 | |
| 		chartVersion *repo.ChartVersion
 | |
| 		wantURL      string
 | |
| 		wantErr      bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "relative URL",
 | |
| 			url:  "https://example.com",
 | |
| 			chartVersion: &repo.ChartVersion{
 | |
| 				Metadata: &chart.Metadata{Name: "chart"},
 | |
| 				URLs:     []string{"charts/foo-1.0.0.tgz"},
 | |
| 			},
 | |
| 			wantURL: "https://example.com/charts/foo-1.0.0.tgz",
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "no chart URL",
 | |
| 			chartVersion: &repo.ChartVersion{Metadata: &chart.Metadata{Name: "chart"}},
 | |
| 			wantErr:      true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid chart URL",
 | |
| 			chartVersion: &repo.ChartVersion{
 | |
| 				Metadata: &chart.Metadata{Name: "chart"},
 | |
| 				URLs:     []string{"https://ex ample.com/charts/foo-1.0.0.tgz"},
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		tt := tt
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			mg := mockGetter{}
 | |
| 			r := &ChartRepository{
 | |
| 				URL:    tt.url,
 | |
| 				Client: &mg,
 | |
| 			}
 | |
| 			res, err := r.DownloadChart(tt.chartVersion)
 | |
| 			if tt.wantErr {
 | |
| 				g.Expect(err).To(HaveOccurred())
 | |
| 				g.Expect(res).To(BeNil())
 | |
| 				return
 | |
| 			}
 | |
| 			g.Expect(mg.LastCalledURL).To(Equal(tt.wantURL))
 | |
| 			g.Expect(res).ToNot(BeNil())
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestChartRepository_CacheIndex(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	mg := mockGetter{Response: []byte("foo")}
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	r.URL = "https://example.com"
 | |
| 	r.Client = &mg
 | |
| 	r.digests["key"] = "value"
 | |
| 
 | |
| 	err := r.CacheIndex()
 | |
| 	g.Expect(err).To(Not(HaveOccurred()))
 | |
| 
 | |
| 	g.Expect(r.Path).ToNot(BeEmpty())
 | |
| 	t.Cleanup(func() { _ = os.Remove(r.Path) })
 | |
| 
 | |
| 	g.Expect(r.Path).To(BeARegularFile())
 | |
| 	b, _ := os.ReadFile(r.Path)
 | |
| 	g.Expect(b).To(Equal(mg.Response))
 | |
| 
 | |
| 	g.Expect(r.digests).To(BeEmpty())
 | |
| }
 | |
| 
 | |
| func TestChartRepository_ToJSON(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	r.Path = chartmuseumTestFile
 | |
| 
 | |
| 	_, err := r.ToJSON()
 | |
| 	g.Expect(err).To(HaveOccurred())
 | |
| 
 | |
| 	g.Expect(r.LoadFromPath()).To(Succeed())
 | |
| 	b, err := r.ToJSON()
 | |
| 	g.Expect(err).ToNot(HaveOccurred())
 | |
| 
 | |
| 	jsonBytes, err := os.ReadFile(chartmuseumJSONTestFile)
 | |
| 	jsonBytes = bytes.TrimRight(jsonBytes, "\n")
 | |
| 	g.Expect(err).To(Not(HaveOccurred()))
 | |
| 	g.Expect(string(b)).To(Equal(string(jsonBytes)))
 | |
| }
 | |
| 
 | |
| func TestChartRepository_DownloadIndex(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	b, err := os.ReadFile(chartmuseumTestFile)
 | |
| 	g.Expect(err).ToNot(HaveOccurred())
 | |
| 
 | |
| 	mg := mockGetter{Response: b}
 | |
| 	r := &ChartRepository{
 | |
| 		URL:     "https://example.com",
 | |
| 		Client:  &mg,
 | |
| 		RWMutex: &sync.RWMutex{},
 | |
| 	}
 | |
| 
 | |
| 	t.Run("download index", func(t *testing.T) {
 | |
| 		buf := bytes.NewBuffer([]byte{})
 | |
| 		g.Expect(r.DownloadIndex(buf, helm.MaxIndexSize)).To(Succeed())
 | |
| 		g.Expect(buf.Bytes()).To(Equal(b))
 | |
| 		g.Expect(mg.LastCalledURL).To(Equal(r.URL + "/index.yaml"))
 | |
| 		g.Expect(err).To(BeNil())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("download index size error", func(t *testing.T) {
 | |
| 		buf := bytes.NewBuffer([]byte{})
 | |
| 		g.Expect(r.DownloadIndex(buf, int64(len(b)-1))).To(HaveOccurred())
 | |
| 		g.Expect(mg.LastCalledURL).To(Equal(r.URL + "/index.yaml"))
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_StrategicallyLoadIndex(t *testing.T) {
 | |
| 	t.Run("loads from path", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		i := filepath.Join(t.TempDir(), "index.yaml")
 | |
| 		g.Expect(os.WriteFile(i, []byte(`apiVersion: v1`), 0o600)).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = i
 | |
| 
 | |
| 		err := r.StrategicallyLoadIndex()
 | |
| 		g.Expect(err).To(Succeed())
 | |
| 		g.Expect(r.Index).ToNot(BeNil())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("loads from client", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Client = &mockGetter{
 | |
| 			Response: []byte(`apiVersion: v1`),
 | |
| 		}
 | |
| 		t.Cleanup(func() {
 | |
| 			_ = os.Remove(r.Path)
 | |
| 		})
 | |
| 
 | |
| 		err := r.StrategicallyLoadIndex()
 | |
| 		g.Expect(err).To(Succeed())
 | |
| 		g.Expect(r.Path).ToNot(BeEmpty())
 | |
| 		g.Expect(r.Index).ToNot(BeNil())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("skips if index is already loaded", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Index = repo.NewIndexFile()
 | |
| 
 | |
| 		g.Expect(r.StrategicallyLoadIndex()).To(Succeed())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_LoadFromPath(t *testing.T) {
 | |
| 	t.Run("loads index", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		i := filepath.Join(t.TempDir(), "index.yaml")
 | |
| 		g.Expect(os.WriteFile(i, []byte(`apiVersion: v1`), 0o600)).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = i
 | |
| 
 | |
| 		g.Expect(r.LoadFromPath()).To(Succeed())
 | |
| 		g.Expect(r.Index).ToNot(BeNil())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("no cache path", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		err := newChartRepository().LoadFromPath()
 | |
| 		g.Expect(err).To(HaveOccurred())
 | |
| 		g.Expect(err.Error()).To(ContainSubstring("no cache path"))
 | |
| 	})
 | |
| 
 | |
| 	t.Run("index load error", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = filepath.Join(t.TempDir(), "index.yaml")
 | |
| 
 | |
| 		err := r.LoadFromPath()
 | |
| 		g.Expect(err).To(HaveOccurred())
 | |
| 		g.Expect(errors.Is(err, os.ErrNotExist)).To(BeTrue())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_Digest(t *testing.T) {
 | |
| 	t.Run("with algorithm", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		p := filepath.Join(t.TempDir(), "index.yaml")
 | |
| 		g.Expect(repo.NewIndexFile().WriteFile(p, 0o600)).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = p
 | |
| 
 | |
| 		for _, algo := range []digest.Algorithm{digest.SHA256, digest.SHA512} {
 | |
| 			t.Run(algo.String(), func(t *testing.T) {
 | |
| 				g := NewWithT(t)
 | |
| 
 | |
| 				d := r.Digest(algo)
 | |
| 				g.Expect(d).ToNot(BeEmpty())
 | |
| 				g.Expect(d.Algorithm()).To(Equal(algo))
 | |
| 				g.Expect(r.digests[algo]).To(Equal(d))
 | |
| 			})
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("without path", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		g.Expect(r.Digest(digest.SHA256)).To(BeEmpty())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("from cache", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		algo := digest.SHA256
 | |
| 		expect := digest.Digest("sha256:fake")
 | |
| 
 | |
| 		i := filepath.Join(t.TempDir(), "index.yaml")
 | |
| 		g.Expect(os.WriteFile(i, []byte(`apiVersion: v1`), 0o600)).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = i
 | |
| 		r.digests[algo] = expect
 | |
| 
 | |
| 		g.Expect(r.Digest(algo)).To(Equal(expect))
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_HasIndex(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	g.Expect(r.HasIndex()).To(BeFalse())
 | |
| 	r.Index = repo.NewIndexFile()
 | |
| 	g.Expect(r.HasIndex()).To(BeTrue())
 | |
| }
 | |
| 
 | |
| func TestChartRepository_HasFile(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	g.Expect(r.HasFile()).To(BeFalse())
 | |
| 
 | |
| 	i := filepath.Join(t.TempDir(), "index.yaml")
 | |
| 	g.Expect(os.WriteFile(i, []byte(`apiVersion: v1`), 0o600)).To(Succeed())
 | |
| 	r.Path = i
 | |
| 	g.Expect(r.HasFile()).To(BeTrue())
 | |
| }
 | |
| 
 | |
| func TestChartRepository_Clear(t *testing.T) {
 | |
| 	t.Run("without index", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		g.Expect(r.Clear()).To(Succeed())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("with index", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Index = repo.NewIndexFile()
 | |
| 
 | |
| 		g.Expect(r.Clear()).To(Succeed())
 | |
| 		g.Expect(r.Index).To(BeNil())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("with index and cached path", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		f, err := os.CreateTemp(t.TempDir(), "index-*.yaml")
 | |
| 		g.Expect(err).ToNot(HaveOccurred())
 | |
| 		g.Expect(f.Close()).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = f.Name()
 | |
| 		r.Index = repo.NewIndexFile()
 | |
| 		r.digests["key"] = "value"
 | |
| 		r.cached = true
 | |
| 
 | |
| 		g.Expect(r.Clear()).To(Succeed())
 | |
| 		g.Expect(r.Index).To(BeNil())
 | |
| 		g.Expect(r.Path).To(BeEmpty())
 | |
| 		g.Expect(r.digests).To(BeEmpty())
 | |
| 		g.Expect(r.cached).To(BeFalse())
 | |
| 	})
 | |
| 
 | |
| 	t.Run("with path", func(t *testing.T) {
 | |
| 		g := NewWithT(t)
 | |
| 
 | |
| 		f, err := os.CreateTemp(t.TempDir(), "index-*.yaml")
 | |
| 		g.Expect(err).ToNot(HaveOccurred())
 | |
| 		g.Expect(f.Close()).To(Succeed())
 | |
| 
 | |
| 		r := newChartRepository()
 | |
| 		r.Path = f.Name()
 | |
| 		r.digests["key"] = "value"
 | |
| 
 | |
| 		g.Expect(r.Clear()).To(Succeed())
 | |
| 		g.Expect(r.Path).ToNot(BeEmpty())
 | |
| 		g.Expect(r.Path).To(BeARegularFile())
 | |
| 		g.Expect(r.digests).To(BeEmpty())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestChartRepository_Invalidate(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	r := newChartRepository()
 | |
| 	r.digests["key"] = "value"
 | |
| 
 | |
| 	r.Invalidate()
 | |
| 	g.Expect(r.digests).To(BeEmpty())
 | |
| }
 | |
| 
 | |
| func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	g.Expect(i.Entries).ToNot(BeNil())
 | |
| 	g.Expect(i.Entries).To(HaveLen(4), "expected 4 entries in index file")
 | |
| 
 | |
| 	alpine, ok := i.Entries["alpine"]
 | |
| 	g.Expect(ok).To(BeTrue(), "expected 'alpine' entry to exist")
 | |
| 	g.Expect(alpine).To(HaveLen(1), "'alpine' should have 1 entry")
 | |
| 
 | |
| 	nginx, ok := i.Entries["nginx"]
 | |
| 	g.Expect(ok).To(BeTrue(), "expected 'nginx' entry to exist")
 | |
| 	g.Expect(nginx).To(HaveLen(2), "'nginx' should have 2 entries")
 | |
| 
 | |
| 	broken, ok := i.Entries["xChartWithDuplicateDependenciesAndMissingAlias"]
 | |
| 	g.Expect(ok).To(BeTrue(), "expected 'xChartWithDuplicateDependenciesAndMissingAlias' entry to exist")
 | |
| 	g.Expect(broken).To(HaveLen(1), "'xChartWithDuplicateDependenciesAndMissingAlias' should have 1 entries")
 | |
| 
 | |
| 	expects := []*repo.ChartVersion{
 | |
| 		{
 | |
| 			Metadata: &chart.Metadata{
 | |
| 				Name:        "alpine",
 | |
| 				Description: "string",
 | |
| 				Version:     "1.0.0",
 | |
| 				Keywords:    []string{"linux", "alpine", "small", "sumtin"},
 | |
| 				Home:        "https://github.com/something",
 | |
| 			},
 | |
| 			URLs: []string{
 | |
| 				"https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz",
 | |
| 				"http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz",
 | |
| 			},
 | |
| 			Digest: "sha256:1234567890abcdef",
 | |
| 		},
 | |
| 		{
 | |
| 			Metadata: &chart.Metadata{
 | |
| 				Name:        "nginx",
 | |
| 				Description: "string",
 | |
| 				Version:     "0.2.0",
 | |
| 				Keywords:    []string{"popular", "web server", "proxy"},
 | |
| 				Home:        "https://github.com/something/else",
 | |
| 			},
 | |
| 			URLs: []string{
 | |
| 				"https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz",
 | |
| 			},
 | |
| 			Digest: "sha256:1234567890abcdef",
 | |
| 		},
 | |
| 		{
 | |
| 			Metadata: &chart.Metadata{
 | |
| 				Name:        "nginx",
 | |
| 				Description: "string",
 | |
| 				Version:     "0.1.0",
 | |
| 				Keywords:    []string{"popular", "web server", "proxy"},
 | |
| 				Home:        "https://github.com/something",
 | |
| 			},
 | |
| 			URLs: []string{
 | |
| 				"https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz",
 | |
| 			},
 | |
| 			Digest: "sha256:1234567890abcdef",
 | |
| 		},
 | |
| 		{
 | |
| 			Metadata: &chart.Metadata{
 | |
| 				Name:        "xChartWithDuplicateDependenciesAndMissingAlias",
 | |
| 				Description: "string",
 | |
| 				Version:     "1.2.3",
 | |
| 				Keywords:    []string{"broken", "still accepted"},
 | |
| 				Home:        "https://example.com/something",
 | |
| 				Dependencies: []*chart.Dependency{
 | |
| 					{Name: "kube-rbac-proxy", Version: "0.9.1"},
 | |
| 				},
 | |
| 			},
 | |
| 			URLs: []string{
 | |
| 				"https://kubernetes-charts.storage.googleapis.com/nginx-1.2.3.tgz",
 | |
| 			},
 | |
| 			Digest: "sha256:1234567890abcdef",
 | |
| 		},
 | |
| 	}
 | |
| 	tests := []*repo.ChartVersion{alpine[0], nginx[0], nginx[1], broken[0]}
 | |
| 
 | |
| 	for i, tt := range tests {
 | |
| 		expect := expects[i]
 | |
| 		g.Expect(tt.Name).To(Equal(expect.Name))
 | |
| 		g.Expect(tt.Description).To(Equal(expect.Description))
 | |
| 		g.Expect(tt.Version).To(Equal(expect.Version))
 | |
| 		g.Expect(tt.Digest).To(Equal(expect.Digest))
 | |
| 		g.Expect(tt.Home).To(Equal(expect.Home))
 | |
| 		g.Expect(tt.URLs).To(ContainElements(expect.URLs))
 | |
| 		g.Expect(tt.Keywords).To(ContainElements(expect.Keywords))
 | |
| 		g.Expect(tt.Dependencies).To(ContainElements(expect.Dependencies))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // This code is taken from https://github.com/helm/helm/blob/v3.15.2/pkg/repo/index_test.go#L601
 | |
| // and refers to: https://github.com/helm/helm/issues/12748
 | |
| func TestIgnoreSkippableChartValidationError(t *testing.T) {
 | |
| 	type TestCase struct {
 | |
| 		Input        error
 | |
| 		ErrorSkipped bool
 | |
| 	}
 | |
| 	testCases := map[string]TestCase{
 | |
| 		"nil": {
 | |
| 			Input: nil,
 | |
| 		},
 | |
| 		"generic_error": {
 | |
| 			Input: fmt.Errorf("foo"),
 | |
| 		},
 | |
| 		"non_skipped_validation_error": {
 | |
| 			Input: chart.ValidationError("chart.metadata.type must be application or library"),
 | |
| 		},
 | |
| 		"skipped_validation_error": {
 | |
| 			Input:        chart.ValidationErrorf("more than one dependency with name or alias %q", "foo"),
 | |
| 			ErrorSkipped: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for name, tc := range testCases {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			result := ignoreSkippableChartValidationError(tc.Input)
 | |
| 
 | |
| 			if tc.Input == nil {
 | |
| 				if result != nil {
 | |
| 					t.Error("expected nil result for nil input")
 | |
| 				}
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if tc.ErrorSkipped {
 | |
| 				if result != nil {
 | |
| 					t.Error("expected nil result for skipped error")
 | |
| 				}
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if tc.Input != result {
 | |
| 				t.Error("expected the result equal to input")
 | |
| 			}
 | |
| 
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var indexWithFirstVersionInvalid = `
 | |
| apiVersion: v1
 | |
| entries:
 | |
|   nginx:
 | |
|     - urls:
 | |
|         - https://charts.helm.sh/stable/alpine-1.0.0.tgz
 | |
|         - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
 | |
|       name: nginx
 | |
|       version: 0..1.0
 | |
|       description: string
 | |
|       home: https://github.com/something
 | |
|       digest: "sha256:1234567890abcdef"
 | |
|     - urls:
 | |
|         - https://charts.helm.sh/stable/nginx-0.2.0.tgz
 | |
|       name: nginx
 | |
|       description: string
 | |
|       version: 0.2.0
 | |
|       home: https://github.com/something/else
 | |
|       digest: "sha256:1234567890abcdef"
 | |
| `
 | |
| var indexWithLastVersionInvalid = `
 | |
| apiVersion: v1
 | |
| entries:
 | |
|   nginx:
 | |
|     - urls:
 | |
|         - https://charts.helm.sh/stable/nginx-0.2.0.tgz
 | |
|       name: nginx
 | |
|       description: string
 | |
|       version: 0.2.0
 | |
|       home: https://github.com/something/else
 | |
|       digest: "sha256:1234567890abcdef"
 | |
|     - urls:
 | |
|         - https://charts.helm.sh/stable/alpine-1.0.0.tgz
 | |
|         - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
 | |
|       name: nginx
 | |
|       version: 0..1.0
 | |
|       description: string
 | |
|       home: https://github.com/something
 | |
|       digest: "sha256:1234567890abcdef"
 | |
| `
 | |
| 
 | |
| func TestIndexFromBytes_InvalidEntries(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		source string
 | |
| 		data   string
 | |
| 	}{
 | |
| 		{
 | |
| 			source: "indexWithFirstVersionInvalid",
 | |
| 			data:   indexWithFirstVersionInvalid,
 | |
| 		},
 | |
| 		{
 | |
| 			source: "indexWithLastVersionInvalid",
 | |
| 			data:   indexWithLastVersionInvalid,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tc := range tests {
 | |
| 		t.Run(tc.source, func(t *testing.T) {
 | |
| 			idx, err := IndexFromBytes([]byte(tc.data))
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("unexpected error: %s", err)
 | |
| 			}
 | |
| 			cvs := idx.Entries["nginx"]
 | |
| 			if len(cvs) == 0 {
 | |
| 				t.Error("expected one chart version not to be filtered out")
 | |
| 			}
 | |
| 			for _, v := range cvs {
 | |
| 				if v.Version == "0..1.0" {
 | |
| 					t.Error("malformed version was not filtered out")
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |