internal/helm: add repository cache helpers
This commits adds simple caching capabilities to the `ChartRepository`, which makes it possible to load the `Index` from a defined `CachePath` using `LoadFromCache()`, and to download the index to a new `CachePath` using `CacheIndex()`. In addition, the repository tests have been updated to make use of Gomega, and some missing ones have been added. Signed-off-by: Hidde Beydals <hello@hidde.co>
This commit is contained in:
parent
8537a0f8fa
commit
44c1863334
|
|
@ -18,12 +18,17 @@ package helm
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
|
|
@ -33,20 +38,37 @@ import (
|
|||
"github.com/fluxcd/pkg/version"
|
||||
)
|
||||
|
||||
var ErrNoChartIndex = errors.New("no chart index")
|
||||
|
||||
// ChartRepository represents a Helm chart repository, and the configuration
|
||||
// required to download the chart index, and charts from the repository.
|
||||
// required to download the chart index and charts from the repository.
|
||||
// All methods are thread safe unless defined otherwise.
|
||||
type ChartRepository struct {
|
||||
// URL the ChartRepository's index.yaml can be found at,
|
||||
// without the index.yaml suffix.
|
||||
URL string
|
||||
Index *repo.IndexFile
|
||||
// Client to use while downloading the Index or a chart from the URL.
|
||||
Client getter.Getter
|
||||
// Options to configure the Client with while downloading the Index
|
||||
// or a chart from the URL.
|
||||
Options []getter.Option
|
||||
// CachePath is the path of a cached index.yaml for read-only operations.
|
||||
CachePath string
|
||||
// Index contains a loaded chart repository index if not nil.
|
||||
Index *repo.IndexFile
|
||||
// Checksum contains the SHA256 checksum of the loaded chart repository
|
||||
// index bytes.
|
||||
Checksum string
|
||||
|
||||
*sync.RWMutex
|
||||
}
|
||||
|
||||
// NewChartRepository constructs and returns a new ChartRepository with
|
||||
// the ChartRepository.Client configured to the getter.Getter for the
|
||||
// repository URL scheme. It returns an error on URL parsing failures,
|
||||
// or if there is no getter available for the scheme.
|
||||
func NewChartRepository(repositoryURL string, providers getter.Providers, opts []getter.Option) (*ChartRepository, error) {
|
||||
func NewChartRepository(repositoryURL, cachePath string, providers getter.Providers, opts []getter.Option) (*ChartRepository, error) {
|
||||
r := newChartRepository()
|
||||
u, err := url.Parse(repositoryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -55,17 +77,29 @@ func NewChartRepository(repositoryURL string, providers getter.Providers, opts [
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.URL = repositoryURL
|
||||
r.CachePath = cachePath
|
||||
r.Client = c
|
||||
r.Options = opts
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func newChartRepository() *ChartRepository {
|
||||
return &ChartRepository{
|
||||
URL: repositoryURL,
|
||||
Client: c,
|
||||
Options: opts,
|
||||
}, nil
|
||||
RWMutex: &sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the repo.ChartVersion for the given name, the version is expected
|
||||
// to be a semver.Constraints compatible string. If version is empty, the latest
|
||||
// stable version will be returned and prerelease versions will be ignored.
|
||||
func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
if r.Index == nil {
|
||||
return nil, ErrNoChartIndex
|
||||
}
|
||||
cvs, ok := r.Index.Entries[name]
|
||||
if !ok {
|
||||
return nil, repo.ErrNoChartName
|
||||
|
|
@ -114,7 +148,7 @@ func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
|
|||
lookup[v] = cv
|
||||
}
|
||||
if len(matchedVersions) == 0 {
|
||||
return nil, fmt.Errorf("no chart version found for %s-%s", name, ver)
|
||||
return nil, fmt.Errorf("no '%s' chart with version matching '%s' found", name, ver)
|
||||
}
|
||||
|
||||
// Sort versions
|
||||
|
|
@ -145,7 +179,7 @@ func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
|
|||
// ChartRepository. It returns a bytes.Buffer containing the chart data.
|
||||
func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error) {
|
||||
if len(chart.URLs) == 0 {
|
||||
return nil, fmt.Errorf("chart %q has no downloadable URLs", chart.Name)
|
||||
return nil, fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name)
|
||||
}
|
||||
|
||||
// TODO(hidde): according to the Helm source the first item is not
|
||||
|
|
@ -175,13 +209,9 @@ func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer
|
|||
return r.Client.Get(u.String(), r.Options...)
|
||||
}
|
||||
|
||||
// LoadIndex loads the given bytes into the Index while performing
|
||||
// minimal validity checks. It fails if the API version is not set
|
||||
// (repo.ErrNoAPIVersion), or if the unmarshal fails.
|
||||
//
|
||||
// The logic is derived from and on par with:
|
||||
// https://github.com/helm/helm/blob/v3.3.4/pkg/repo/index.go#L301
|
||||
func (r *ChartRepository) LoadIndex(b []byte) error {
|
||||
// LoadIndexFromBytes loads Index from the given bytes.
|
||||
// It returns a repo.ErrNoAPIVersion error if the API version is not set
|
||||
func (r *ChartRepository) LoadIndexFromBytes(b []byte) error {
|
||||
i := &repo.IndexFile{}
|
||||
if err := yaml.UnmarshalStrict(b, i); err != nil {
|
||||
return err
|
||||
|
|
@ -190,14 +220,68 @@ func (r *ChartRepository) LoadIndex(b []byte) error {
|
|||
return repo.ErrNoAPIVersion
|
||||
}
|
||||
i.SortEntries()
|
||||
|
||||
r.Lock()
|
||||
r.Index = i
|
||||
r.Checksum = fmt.Sprintf("%x", sha256.Sum256(b))
|
||||
r.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromFile reads the file at the given path and loads it into Index.
|
||||
func (r *ChartRepository) LoadFromFile(path string) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.LoadIndexFromBytes(b)
|
||||
}
|
||||
|
||||
// CacheIndex attempts to write the index from the remote into a new temporary file
|
||||
// using DownloadIndex, and sets CachePath.
|
||||
// It returns the SHA256 checksum of the downloaded index bytes, or an error.
|
||||
// The caller is expected to handle the garbage collection of CachePath, and to
|
||||
// load the Index separately using LoadFromCache if required.
|
||||
func (r *ChartRepository) CacheIndex() (string, error) {
|
||||
f, err := os.CreateTemp("", "chart-index-*.yaml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file to cache index to: %w", err)
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
mw := io.MultiWriter(f, h)
|
||||
if err = r.DownloadIndex(mw); err != nil {
|
||||
f.Close()
|
||||
os.RemoveAll(f.Name())
|
||||
return "", fmt.Errorf("failed to cache index to '%s': %w", f.Name(), err)
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
os.RemoveAll(f.Name())
|
||||
return "", fmt.Errorf("failed to close cached index file '%s': %w", f.Name(), err)
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
r.CachePath = f.Name()
|
||||
r.Unlock()
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// LoadFromCache attempts to load the Index from the configured CachePath.
|
||||
// It returns an error if no CachePath is set, or if the load failed.
|
||||
func (r *ChartRepository) LoadFromCache() error {
|
||||
r.RLock()
|
||||
if cachePath := r.CachePath; cachePath != "" {
|
||||
r.RUnlock()
|
||||
return r.LoadFromFile(cachePath)
|
||||
}
|
||||
r.RUnlock()
|
||||
return fmt.Errorf("no cache path set")
|
||||
}
|
||||
|
||||
// DownloadIndex attempts to download the chart repository index using
|
||||
// the Client and set Options, and loads the index file into the Index.
|
||||
// It returns an error on URL parsing and Client failures.
|
||||
func (r *ChartRepository) DownloadIndex() error {
|
||||
// the Client and set Options, and writes the index to the given io.Writer.
|
||||
// It returns an url.Error if the URL failed to parse.
|
||||
func (r *ChartRepository) DownloadIndex(w io.Writer) (err error) {
|
||||
u, err := url.Parse(r.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -205,14 +289,36 @@ func (r *ChartRepository) DownloadIndex() error {
|
|||
u.RawPath = path.Join(u.RawPath, "index.yaml")
|
||||
u.Path = path.Join(u.Path, "index.yaml")
|
||||
|
||||
res, err := r.Client.Get(u.String(), r.Options...)
|
||||
var res *bytes.Buffer
|
||||
res, err = r.Client.Get(u.String(), r.Options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := io.ReadAll(res)
|
||||
if err != nil {
|
||||
if _, err = io.Copy(w, res); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.LoadIndex(b)
|
||||
// HasIndex returns true if the Index is not nil.
|
||||
func (r *ChartRepository) HasIndex() bool {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.Index != nil
|
||||
}
|
||||
|
||||
// HasCacheFile returns true if CachePath is not empty.
|
||||
func (r *ChartRepository) HasCacheFile() bool {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.CachePath != ""
|
||||
}
|
||||
|
||||
// UnloadIndex sets the Index to nil.
|
||||
func (r *ChartRepository) UnloadIndex() {
|
||||
if r != nil {
|
||||
r.Lock()
|
||||
r.Index = nil
|
||||
r.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,45 +18,38 @@ package helm
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
var now = time.Now()
|
||||
|
||||
const (
|
||||
testfile = "testdata/local-index.yaml"
|
||||
chartmuseumtestfile = "testdata/chartmuseum-index.yaml"
|
||||
unorderedtestfile = "testdata/local-index-unordered.yaml"
|
||||
indexWithDuplicates = `
|
||||
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"
|
||||
nginx:
|
||||
- urls:
|
||||
- https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz
|
||||
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
|
||||
name: alpine
|
||||
description: string
|
||||
version: 1.0.0
|
||||
home: https://github.com/something
|
||||
digest: "sha256:1234567890abcdef"
|
||||
`
|
||||
testFile = "testdata/local-index.yaml"
|
||||
chartmuseumTestFile = "testdata/chartmuseum-index.yaml"
|
||||
unorderedTestFile = "testdata/local-index-unordered.yaml"
|
||||
)
|
||||
|
||||
// mockGetter can be used as a simple mocking getter.Getter implementation.
|
||||
type mockGetter struct {
|
||||
requestedURL string
|
||||
response []byte
|
||||
}
|
||||
|
||||
func (g *mockGetter) Get(url string, _ ...getter.Option) (*bytes.Buffer, error) {
|
||||
g.requestedURL = url
|
||||
return bytes.NewBuffer(g.response), nil
|
||||
}
|
||||
|
||||
func TestNewChartRepository(t *testing.T) {
|
||||
repositoryURL := "https://example.com"
|
||||
providers := getter.Providers{
|
||||
|
|
@ -68,60 +61,74 @@ func TestNewChartRepository(t *testing.T) {
|
|||
options := []getter.Option{getter.WithBasicAuth("username", "password")}
|
||||
|
||||
t.Run("should construct chart repository", func(t *testing.T) {
|
||||
r, err := NewChartRepository(repositoryURL, providers, options)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if got := r.URL; got != repositoryURL {
|
||||
t.Fatalf("Expecting %q repository URL, got: %q", repositoryURL, got)
|
||||
}
|
||||
if r.Client == nil {
|
||||
t.Fatalf("Expecting client, got nil")
|
||||
}
|
||||
if !reflect.DeepEqual(r.Options, options) {
|
||||
t.Fatalf("Client options mismatth")
|
||||
}
|
||||
g := NewWithT(t)
|
||||
|
||||
r, err := NewChartRepository(repositoryURL, "", providers, 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) {
|
||||
_, err := NewChartRepository("https://ex ample.com", nil, nil)
|
||||
switch err.(type) {
|
||||
case *url.Error:
|
||||
default:
|
||||
t.Fatalf("Expecting URL error, got: %v", err)
|
||||
}
|
||||
g := NewWithT(t)
|
||||
r, err := NewChartRepository("https://ex ample.com", "", 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) {
|
||||
_, err := NewChartRepository("http://example.com", providers, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("Expecting unsupported scheme error")
|
||||
}
|
||||
g := NewWithT(t)
|
||||
|
||||
r, err := NewChartRepository("http://example.com", "", providers, nil)
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal("scheme \"http\" not supported"))
|
||||
g.Expect(r).To(BeNil())
|
||||
})
|
||||
}
|
||||
|
||||
func TestChartRepository_Get(t *testing.T) {
|
||||
i := repo.NewIndexFile()
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.0.1"}, "chart-0.0.1.tgz", "http://example.com/charts", "sha256:1234567890")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.0"}, "chart-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.1"}, "chart-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+b.min.minute"}, "chart-0.1.5+b.min.minute.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Minute)
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+a.min.hour"}, "chart-0.1.5+a.min.hour.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Hour)
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.5+c.now"}, "chart-0.1.5+c.now.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.2.0"}, "chart-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "1.0.0"}, "chart-1.0.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.Add(&chart.Metadata{Name: "chart", Version: "1.1.0-rc.1"}, "chart-1.1.0-rc.1.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
||||
i.SortEntries()
|
||||
r := &ChartRepository{Index: i}
|
||||
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 bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "exact match",
|
||||
|
|
@ -151,12 +158,12 @@ func TestChartRepository_Get(t *testing.T) {
|
|||
name: "unfulfilled range",
|
||||
chartName: "chart",
|
||||
chartVersion: ">2.0.0",
|
||||
wantErr: true,
|
||||
wantErr: "no 'chart' chart with version matching '>2.0.0' found",
|
||||
},
|
||||
{
|
||||
name: "invalid chart",
|
||||
chartName: "non-existing",
|
||||
wantErr: true,
|
||||
wantErr: repo.ErrNoChartName.Error(),
|
||||
},
|
||||
{
|
||||
name: "match newest if ambiguous",
|
||||
|
|
@ -168,14 +175,19 @@ func TestChartRepository_Get(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
cv, err := r.Get(tt.chartName, tt.chartVersion)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(cv).To(BeNil())
|
||||
return
|
||||
}
|
||||
if err == nil && !strings.Contains(cv.Metadata.Version, tt.wantVersion) {
|
||||
t.Errorf("Get() unexpected version = %s, want = %s", cv.Metadata.Version, tt.wantVersion)
|
||||
}
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -212,117 +224,257 @@ func TestChartRepository_DownloadChart(t *testing.T) {
|
|||
},
|
||||
}
|
||||
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,
|
||||
}
|
||||
_, err := r.DownloadChart(tt.chartVersion)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DownloadChart() error = %v, wantErr %v", err, tt.wantErr)
|
||||
res, err := r.DownloadChart(tt.chartVersion)
|
||||
if tt.wantErr {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(res).To(BeNil())
|
||||
return
|
||||
}
|
||||
if err == nil && mg.requestedURL != tt.wantURL {
|
||||
t.Errorf("DownloadChart() requested URL = %s, wantURL %s", mg.requestedURL, tt.wantURL)
|
||||
}
|
||||
g.Expect(mg.requestedURL).To(Equal(tt.wantURL))
|
||||
g.Expect(res).ToNot(BeNil())
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartRepository_DownloadIndex(t *testing.T) {
|
||||
b, err := os.ReadFile(chartmuseumtestfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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,
|
||||
}
|
||||
if err := r.DownloadIndex(); err != nil {
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
g.Expect(r.DownloadIndex(buf)).To(Succeed())
|
||||
g.Expect(buf.Bytes()).To(Equal(b))
|
||||
g.Expect(mg.requestedURL).To(Equal(r.URL + "/index.yaml"))
|
||||
g.Expect(err).To(BeNil())
|
||||
}
|
||||
|
||||
func TestChartRepository_LoadIndexFromBytes(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()
|
||||
|
||||
r := newChartRepository()
|
||||
err := r.LoadIndexFromBytes(tt.b)
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(r.Index).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(r.Index).ToNot(BeNil())
|
||||
got, err := r.Index.Get(tt.wantName, tt.wantVersion)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(got.Digest).To(Equal(tt.wantDigest))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartRepository_LoadIndexFromBytes_Unordered(t *testing.T) {
|
||||
b, err := os.ReadFile(unorderedTestFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if expected := r.URL + "/index.yaml"; mg.requestedURL != expected {
|
||||
t.Errorf("DownloadIndex() requested URL = %s, wantURL %s", mg.requestedURL, expected)
|
||||
r := newChartRepository()
|
||||
err = r.LoadIndexFromBytes(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyLocalIndex(t, r.Index)
|
||||
}
|
||||
|
||||
// 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 TestChartRepository_LoadIndex(t *testing.T) {
|
||||
func TestChartRepository_LoadIndexFromFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
}{
|
||||
{
|
||||
name: "regular index file",
|
||||
filename: testfile,
|
||||
filename: testFile,
|
||||
},
|
||||
{
|
||||
name: "chartmuseum index file",
|
||||
filename: chartmuseumtestfile,
|
||||
filename: chartmuseumTestFile,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
t.Parallel()
|
||||
b, err := os.ReadFile(tt.filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &ChartRepository{}
|
||||
err = r.LoadIndex(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := newChartRepository()
|
||||
err := r.LoadFromFile(testFile)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
verifyLocalIndex(t, r.Index)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartRepository_LoadIndex_Duplicates(t *testing.T) {
|
||||
r := &ChartRepository{}
|
||||
if err := r.LoadIndex([]byte(indexWithDuplicates)); err == nil {
|
||||
t.Errorf("Expected an error when duplicate entries are present")
|
||||
func TestChartRepository_CacheIndex(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
mg := mockGetter{response: []byte("foo")}
|
||||
expectSum := fmt.Sprintf("%x", sha256.Sum256(mg.response))
|
||||
|
||||
r := newChartRepository()
|
||||
r.URL = "https://example.com"
|
||||
r.Client = &mg
|
||||
|
||||
sum, err := r.CacheIndex()
|
||||
g.Expect(err).To(Not(HaveOccurred()))
|
||||
|
||||
g.Expect(r.CachePath).ToNot(BeEmpty())
|
||||
defer os.RemoveAll(r.CachePath)
|
||||
g.Expect(r.CachePath).To(BeARegularFile())
|
||||
b, _ := os.ReadFile(r.CachePath)
|
||||
|
||||
g.Expect(b).To(Equal(mg.response))
|
||||
g.Expect(sum).To(BeEquivalentTo(expectSum))
|
||||
}
|
||||
|
||||
func TestChartRepository_LoadIndexFromCache(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cachePath string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "cache path",
|
||||
cachePath: chartmuseumTestFile,
|
||||
},
|
||||
{
|
||||
name: "invalid cache path",
|
||||
cachePath: "invalid",
|
||||
wantErr: "open invalid: no such file",
|
||||
},
|
||||
{
|
||||
name: "no cache path",
|
||||
cachePath: "",
|
||||
wantErr: "no cache path set",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
r := newChartRepository()
|
||||
r.CachePath = tt.cachePath
|
||||
err := r.LoadFromCache()
|
||||
if tt.wantErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||
g.Expect(r.Index).To(BeNil())
|
||||
return
|
||||
}
|
||||
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
verifyLocalIndex(t, r.Index)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartRepository_LoadIndex_Unordered(t *testing.T) {
|
||||
b, err := os.ReadFile(unorderedtestfile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
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())
|
||||
}
|
||||
r := &ChartRepository{}
|
||||
err = r.LoadIndex(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyLocalIndex(t, r.Index)
|
||||
|
||||
func TestChartRepository_UnloadIndex(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
r := newChartRepository()
|
||||
g.Expect(r.HasIndex()).To(BeFalse())
|
||||
r.Index = repo.NewIndexFile()
|
||||
r.UnloadIndex()
|
||||
g.Expect(r.Index).To(BeNil())
|
||||
}
|
||||
|
||||
func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
|
||||
numEntries := len(i.Entries)
|
||||
if numEntries != 3 {
|
||||
t.Errorf("Expected 3 entries in index file but got %d", numEntries)
|
||||
}
|
||||
g := NewWithT(t)
|
||||
|
||||
g.Expect(i.Entries).ToNot(BeNil())
|
||||
g.Expect(i.Entries).To(HaveLen(3), "expected 3 entries in index file")
|
||||
|
||||
alpine, ok := i.Entries["alpine"]
|
||||
if !ok {
|
||||
t.Fatalf("'alpine' section not found.")
|
||||
}
|
||||
|
||||
if l := len(alpine); l != 1 {
|
||||
t.Fatalf("'alpine' should have 1 chart, got %d", l)
|
||||
}
|
||||
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"]
|
||||
if !ok || len(nginx) != 2 {
|
||||
t.Fatalf("Expected 2 nginx entries")
|
||||
}
|
||||
g.Expect(ok).To(BeTrue(), "expected 'nginx' entry to exist")
|
||||
g.Expect(nginx).To(HaveLen(2), "'nginx' should have 2 entries")
|
||||
|
||||
expects := []*repo.ChartVersion{
|
||||
{
|
||||
|
|
@ -370,41 +522,12 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
|
|||
|
||||
for i, tt := range tests {
|
||||
expect := expects[i]
|
||||
if tt.Name != expect.Name {
|
||||
t.Errorf("Expected name %q, got %q", expect.Name, tt.Name)
|
||||
}
|
||||
if tt.Description != expect.Description {
|
||||
t.Errorf("Expected description %q, got %q", expect.Description, tt.Description)
|
||||
}
|
||||
if tt.Version != expect.Version {
|
||||
t.Errorf("Expected version %q, got %q", expect.Version, tt.Version)
|
||||
}
|
||||
if tt.Digest != expect.Digest {
|
||||
t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest)
|
||||
}
|
||||
if tt.Home != expect.Home {
|
||||
t.Errorf("Expected home %q, got %q", expect.Home, tt.Home)
|
||||
}
|
||||
|
||||
for i, url := range tt.URLs {
|
||||
if url != expect.URLs[i] {
|
||||
t.Errorf("Expected URL %q, got %q", expect.URLs[i], url)
|
||||
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))
|
||||
}
|
||||
}
|
||||
for i, kw := range tt.Keywords {
|
||||
if kw != expect.Keywords[i] {
|
||||
t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type mockGetter struct {
|
||||
requestedURL string
|
||||
response []byte
|
||||
}
|
||||
|
||||
func (g *mockGetter) Get(url string, options ...getter.Option) (*bytes.Buffer, error) {
|
||||
g.requestedURL = url
|
||||
return bytes.NewBuffer(g.response), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2021 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 helm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestNormalizeChartRepositoryURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "with slash",
|
||||
url: "http://example.com/",
|
||||
want: "http://example.com/",
|
||||
},
|
||||
{
|
||||
name: "without slash",
|
||||
url: "http://example.com",
|
||||
want: "http://example.com/",
|
||||
},
|
||||
{
|
||||
name: "double slash",
|
||||
url: "http://example.com//",
|
||||
want: "http://example.com/",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
url: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
got := NormalizeChartRepositoryURL(tt.url)
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue