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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"helm.sh/helm/v3/pkg/getter"
|
"helm.sh/helm/v3/pkg/getter"
|
||||||
|
|
@ -33,20 +38,37 @@ import (
|
||||||
"github.com/fluxcd/pkg/version"
|
"github.com/fluxcd/pkg/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNoChartIndex = errors.New("no chart index")
|
||||||
|
|
||||||
// ChartRepository represents a Helm chart repository, and the configuration
|
// 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 {
|
type ChartRepository struct {
|
||||||
URL string
|
// URL the ChartRepository's index.yaml can be found at,
|
||||||
Index *repo.IndexFile
|
// without the index.yaml suffix.
|
||||||
Client getter.Getter
|
URL string
|
||||||
|
// 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
|
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
|
// NewChartRepository constructs and returns a new ChartRepository with
|
||||||
// the ChartRepository.Client configured to the getter.Getter for the
|
// the ChartRepository.Client configured to the getter.Getter for the
|
||||||
// repository URL scheme. It returns an error on URL parsing failures,
|
// repository URL scheme. It returns an error on URL parsing failures,
|
||||||
// or if there is no getter available for the scheme.
|
// 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)
|
u, err := url.Parse(repositoryURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -55,17 +77,29 @@ func NewChartRepository(repositoryURL string, providers getter.Providers, opts [
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
r.URL = repositoryURL
|
||||||
|
r.CachePath = cachePath
|
||||||
|
r.Client = c
|
||||||
|
r.Options = opts
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newChartRepository() *ChartRepository {
|
||||||
return &ChartRepository{
|
return &ChartRepository{
|
||||||
URL: repositoryURL,
|
RWMutex: &sync.RWMutex{},
|
||||||
Client: c,
|
}
|
||||||
Options: opts,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the repo.ChartVersion for the given name, the version is expected
|
// 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
|
// to be a semver.Constraints compatible string. If version is empty, the latest
|
||||||
// stable version will be returned and prerelease versions will be ignored.
|
// stable version will be returned and prerelease versions will be ignored.
|
||||||
func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
|
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]
|
cvs, ok := r.Index.Entries[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, repo.ErrNoChartName
|
return nil, repo.ErrNoChartName
|
||||||
|
|
@ -114,7 +148,7 @@ func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
|
||||||
lookup[v] = cv
|
lookup[v] = cv
|
||||||
}
|
}
|
||||||
if len(matchedVersions) == 0 {
|
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
|
// 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.
|
// ChartRepository. It returns a bytes.Buffer containing the chart data.
|
||||||
func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error) {
|
func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error) {
|
||||||
if len(chart.URLs) == 0 {
|
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
|
// 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...)
|
return r.Client.Get(u.String(), r.Options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIndex loads the given bytes into the Index while performing
|
// LoadIndexFromBytes loads Index from the given bytes.
|
||||||
// minimal validity checks. It fails if the API version is not set
|
// It returns a repo.ErrNoAPIVersion error if the API version is not set
|
||||||
// (repo.ErrNoAPIVersion), or if the unmarshal fails.
|
func (r *ChartRepository) LoadIndexFromBytes(b []byte) error {
|
||||||
//
|
|
||||||
// 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 {
|
|
||||||
i := &repo.IndexFile{}
|
i := &repo.IndexFile{}
|
||||||
if err := yaml.UnmarshalStrict(b, i); err != nil {
|
if err := yaml.UnmarshalStrict(b, i); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -190,14 +220,68 @@ func (r *ChartRepository) LoadIndex(b []byte) error {
|
||||||
return repo.ErrNoAPIVersion
|
return repo.ErrNoAPIVersion
|
||||||
}
|
}
|
||||||
i.SortEntries()
|
i.SortEntries()
|
||||||
|
|
||||||
|
r.Lock()
|
||||||
r.Index = i
|
r.Index = i
|
||||||
|
r.Checksum = fmt.Sprintf("%x", sha256.Sum256(b))
|
||||||
|
r.Unlock()
|
||||||
return nil
|
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
|
// DownloadIndex attempts to download the chart repository index using
|
||||||
// the Client and set Options, and loads the index file into the Index.
|
// the Client and set Options, and writes the index to the given io.Writer.
|
||||||
// It returns an error on URL parsing and Client failures.
|
// It returns an url.Error if the URL failed to parse.
|
||||||
func (r *ChartRepository) DownloadIndex() error {
|
func (r *ChartRepository) DownloadIndex(w io.Writer) (err error) {
|
||||||
u, err := url.Parse(r.URL)
|
u, err := url.Parse(r.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -205,14 +289,36 @@ func (r *ChartRepository) DownloadIndex() error {
|
||||||
u.RawPath = path.Join(u.RawPath, "index.yaml")
|
u.RawPath = path.Join(u.RawPath, "index.yaml")
|
||||||
u.Path = path.Join(u.Path, "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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
b, err := io.ReadAll(res)
|
if _, err = io.Copy(w, res); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
"helm.sh/helm/v3/pkg/chart"
|
"helm.sh/helm/v3/pkg/chart"
|
||||||
"helm.sh/helm/v3/pkg/getter"
|
"helm.sh/helm/v3/pkg/getter"
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var now = time.Now()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testfile = "testdata/local-index.yaml"
|
testFile = "testdata/local-index.yaml"
|
||||||
chartmuseumtestfile = "testdata/chartmuseum-index.yaml"
|
chartmuseumTestFile = "testdata/chartmuseum-index.yaml"
|
||||||
unorderedtestfile = "testdata/local-index-unordered.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"
|
|
||||||
`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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) {
|
func TestNewChartRepository(t *testing.T) {
|
||||||
repositoryURL := "https://example.com"
|
repositoryURL := "https://example.com"
|
||||||
providers := getter.Providers{
|
providers := getter.Providers{
|
||||||
|
|
@ -68,60 +61,74 @@ func TestNewChartRepository(t *testing.T) {
|
||||||
options := []getter.Option{getter.WithBasicAuth("username", "password")}
|
options := []getter.Option{getter.WithBasicAuth("username", "password")}
|
||||||
|
|
||||||
t.Run("should construct chart repository", func(t *testing.T) {
|
t.Run("should construct chart repository", func(t *testing.T) {
|
||||||
r, err := NewChartRepository(repositoryURL, providers, options)
|
g := NewWithT(t)
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
r, err := NewChartRepository(repositoryURL, "", providers, options)
|
||||||
}
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
if got := r.URL; got != repositoryURL {
|
g.Expect(r).ToNot(BeNil())
|
||||||
t.Fatalf("Expecting %q repository URL, got: %q", repositoryURL, got)
|
g.Expect(r.URL).To(Equal(repositoryURL))
|
||||||
}
|
g.Expect(r.Client).ToNot(BeNil())
|
||||||
if r.Client == nil {
|
g.Expect(r.Options).To(Equal(options))
|
||||||
t.Fatalf("Expecting client, got nil")
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(r.Options, options) {
|
|
||||||
t.Fatalf("Client options mismatth")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should error on URL parsing failure", func(t *testing.T) {
|
t.Run("should error on URL parsing failure", func(t *testing.T) {
|
||||||
_, err := NewChartRepository("https://ex ample.com", nil, nil)
|
g := NewWithT(t)
|
||||||
switch err.(type) {
|
r, err := NewChartRepository("https://ex ample.com", "", nil, nil)
|
||||||
case *url.Error:
|
g.Expect(err).To(HaveOccurred())
|
||||||
default:
|
g.Expect(err).To(BeAssignableToTypeOf(&url.Error{}))
|
||||||
t.Fatalf("Expecting URL error, got: %v", err)
|
g.Expect(r).To(BeNil())
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should error on unsupported scheme", func(t *testing.T) {
|
t.Run("should error on unsupported scheme", func(t *testing.T) {
|
||||||
_, err := NewChartRepository("http://example.com", providers, nil)
|
g := NewWithT(t)
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expecting unsupported scheme error")
|
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) {
|
func TestChartRepository_Get(t *testing.T) {
|
||||||
i := repo.NewIndexFile()
|
g := NewWithT(t)
|
||||||
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")
|
r := newChartRepository()
|
||||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.1.1"}, "chart-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
r.Index = repo.NewIndexFile()
|
||||||
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")
|
charts := []struct {
|
||||||
i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Minute)
|
name string
|
||||||
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")
|
version string
|
||||||
i.Entries["chart"][len(i.Entries["chart"])-1].Created = time.Now().Add(-time.Hour)
|
url string
|
||||||
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")
|
digest string
|
||||||
i.Add(&chart.Metadata{Name: "chart", Version: "0.2.0"}, "chart-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
|
created time.Time
|
||||||
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")
|
{name: "chart", version: "0.0.1", url: "http://example.com/charts", digest: "sha256:1234567890"},
|
||||||
i.SortEntries()
|
{name: "chart", version: "0.1.0", url: "http://example.com/charts", digest: "sha256:1234567890abc"},
|
||||||
r := &ChartRepository{Index: i}
|
{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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
chartName string
|
chartName string
|
||||||
chartVersion string
|
chartVersion string
|
||||||
wantVersion string
|
wantVersion string
|
||||||
wantErr bool
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "exact match",
|
name: "exact match",
|
||||||
|
|
@ -151,12 +158,12 @@ func TestChartRepository_Get(t *testing.T) {
|
||||||
name: "unfulfilled range",
|
name: "unfulfilled range",
|
||||||
chartName: "chart",
|
chartName: "chart",
|
||||||
chartVersion: ">2.0.0",
|
chartVersion: ">2.0.0",
|
||||||
wantErr: true,
|
wantErr: "no 'chart' chart with version matching '>2.0.0' found",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid chart",
|
name: "invalid chart",
|
||||||
chartName: "non-existing",
|
chartName: "non-existing",
|
||||||
wantErr: true,
|
wantErr: repo.ErrNoChartName.Error(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "match newest if ambiguous",
|
name: "match newest if ambiguous",
|
||||||
|
|
@ -168,14 +175,19 @@ func TestChartRepository_Get(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
cv, err := r.Get(tt.chartName, tt.chartVersion)
|
cv, err := r.Get(tt.chartName, tt.chartVersion)
|
||||||
if (err != nil) != tt.wantErr {
|
if tt.wantErr != "" {
|
||||||
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr))
|
||||||
|
g.Expect(cv).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err == nil && !strings.Contains(cv.Metadata.Version, tt.wantVersion) {
|
g.Expect(cv).ToNot(BeNil())
|
||||||
t.Errorf("Get() unexpected version = %s, want = %s", cv.Metadata.Version, tt.wantVersion)
|
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 {
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
mg := mockGetter{}
|
mg := mockGetter{}
|
||||||
r := &ChartRepository{
|
r := &ChartRepository{
|
||||||
URL: tt.url,
|
URL: tt.url,
|
||||||
Client: &mg,
|
Client: &mg,
|
||||||
}
|
}
|
||||||
_, err := r.DownloadChart(tt.chartVersion)
|
res, err := r.DownloadChart(tt.chartVersion)
|
||||||
if (err != nil) != tt.wantErr {
|
if tt.wantErr {
|
||||||
t.Errorf("DownloadChart() error = %v, wantErr %v", err, tt.wantErr)
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
g.Expect(res).To(BeNil())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err == nil && mg.requestedURL != tt.wantURL {
|
g.Expect(mg.requestedURL).To(Equal(tt.wantURL))
|
||||||
t.Errorf("DownloadChart() requested URL = %s, wantURL %s", mg.requestedURL, tt.wantURL)
|
g.Expect(res).ToNot(BeNil())
|
||||||
}
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChartRepository_DownloadIndex(t *testing.T) {
|
func TestChartRepository_DownloadIndex(t *testing.T) {
|
||||||
b, err := os.ReadFile(chartmuseumtestfile)
|
g := NewWithT(t)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
b, err := os.ReadFile(chartmuseumTestFile)
|
||||||
}
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
mg := mockGetter{response: b}
|
mg := mockGetter{response: b}
|
||||||
r := &ChartRepository{
|
r := &ChartRepository{
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Client: &mg,
|
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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if expected := r.URL + "/index.yaml"; mg.requestedURL != expected {
|
r := newChartRepository()
|
||||||
t.Errorf("DownloadIndex() requested URL = %s, wantURL %s", mg.requestedURL, expected)
|
err = r.LoadIndexFromBytes(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
verifyLocalIndex(t, r.Index)
|
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
|
// 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.
|
// to ensure parity with Helm behaviour.
|
||||||
func TestChartRepository_LoadIndex(t *testing.T) {
|
func TestChartRepository_LoadIndexFromFile(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
filename string
|
filename string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "regular index file",
|
name: "regular index file",
|
||||||
filename: testfile,
|
filename: testFile,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "chartmuseum index file",
|
name: "chartmuseum index file",
|
||||||
filename: chartmuseumtestfile,
|
filename: chartmuseumTestFile,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
tt := tt
|
tt := tt
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
b, err := os.ReadFile(tt.filename)
|
|
||||||
if err != nil {
|
r := newChartRepository()
|
||||||
t.Fatal(err)
|
err := r.LoadFromFile(testFile)
|
||||||
}
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
r := &ChartRepository{}
|
|
||||||
err = r.LoadIndex(b)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
verifyLocalIndex(t, r.Index)
|
verifyLocalIndex(t, r.Index)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChartRepository_LoadIndex_Duplicates(t *testing.T) {
|
func TestChartRepository_CacheIndex(t *testing.T) {
|
||||||
r := &ChartRepository{}
|
g := NewWithT(t)
|
||||||
if err := r.LoadIndex([]byte(indexWithDuplicates)); err == nil {
|
|
||||||
t.Errorf("Expected an error when duplicate entries are present")
|
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) {
|
func TestChartRepository_HasIndex(t *testing.T) {
|
||||||
b, err := os.ReadFile(unorderedtestfile)
|
g := NewWithT(t)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
r := newChartRepository()
|
||||||
}
|
g.Expect(r.HasIndex()).To(BeFalse())
|
||||||
r := &ChartRepository{}
|
r.Index = repo.NewIndexFile()
|
||||||
err = r.LoadIndex(b)
|
g.Expect(r.HasIndex()).To(BeTrue())
|
||||||
if err != nil {
|
}
|
||||||
t.Fatal(err)
|
|
||||||
}
|
func TestChartRepository_UnloadIndex(t *testing.T) {
|
||||||
verifyLocalIndex(t, r.Index)
|
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) {
|
func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
|
||||||
numEntries := len(i.Entries)
|
g := NewWithT(t)
|
||||||
if numEntries != 3 {
|
|
||||||
t.Errorf("Expected 3 entries in index file but got %d", numEntries)
|
g.Expect(i.Entries).ToNot(BeNil())
|
||||||
}
|
g.Expect(i.Entries).To(HaveLen(3), "expected 3 entries in index file")
|
||||||
|
|
||||||
alpine, ok := i.Entries["alpine"]
|
alpine, ok := i.Entries["alpine"]
|
||||||
if !ok {
|
g.Expect(ok).To(BeTrue(), "expected 'alpine' entry to exist")
|
||||||
t.Fatalf("'alpine' section not found.")
|
g.Expect(alpine).To(HaveLen(1), "'alpine' should have 1 entry")
|
||||||
}
|
|
||||||
|
|
||||||
if l := len(alpine); l != 1 {
|
|
||||||
t.Fatalf("'alpine' should have 1 chart, got %d", l)
|
|
||||||
}
|
|
||||||
|
|
||||||
nginx, ok := i.Entries["nginx"]
|
nginx, ok := i.Entries["nginx"]
|
||||||
if !ok || len(nginx) != 2 {
|
g.Expect(ok).To(BeTrue(), "expected 'nginx' entry to exist")
|
||||||
t.Fatalf("Expected 2 nginx entries")
|
g.Expect(nginx).To(HaveLen(2), "'nginx' should have 2 entries")
|
||||||
}
|
|
||||||
|
|
||||||
expects := []*repo.ChartVersion{
|
expects := []*repo.ChartVersion{
|
||||||
{
|
{
|
||||||
|
|
@ -370,41 +522,12 @@ func verifyLocalIndex(t *testing.T, i *repo.IndexFile) {
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
expect := expects[i]
|
expect := expects[i]
|
||||||
if tt.Name != expect.Name {
|
g.Expect(tt.Name).To(Equal(expect.Name))
|
||||||
t.Errorf("Expected name %q, got %q", expect.Name, tt.Name)
|
g.Expect(tt.Description).To(Equal(expect.Description))
|
||||||
}
|
g.Expect(tt.Version).To(Equal(expect.Version))
|
||||||
if tt.Description != expect.Description {
|
g.Expect(tt.Digest).To(Equal(expect.Digest))
|
||||||
t.Errorf("Expected description %q, got %q", expect.Description, tt.Description)
|
g.Expect(tt.Home).To(Equal(expect.Home))
|
||||||
}
|
g.Expect(tt.URLs).To(ContainElements(expect.URLs))
|
||||||
if tt.Version != expect.Version {
|
g.Expect(tt.Keywords).To(ContainElements(expect.Keywords))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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