source-controller/internal/helm/repository_test.go

399 lines
10 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 helm
import (
"bytes"
"io/ioutil"
"net/url"
"reflect"
"strings"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
)
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"
`
)
func TestNewChartRepository(t *testing.T) {
repositoryURL := "https://example.com"
providers := getter.Providers{
getter.Provider{
Schemes: []string{"https"},
New: getter.NewHTTPGetter,
},
}
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")
}
})
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)
}
})
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")
}
})
}
func TestChartRepository_Get(t *testing.T) {
i := repo.NewIndexFile()
i.Add(&chart.Metadata{Name: "chart", Version: "exact"}, "chart-exact.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.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}
tests := []struct {
name string
chartName string
chartVersion string
wantVersion string
wantErr bool
}{
{
name: "exact matth",
chartName: "chart",
chartVersion: "exact",
wantVersion: "exact",
},
{
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: true,
},
{
name: "invalid chart",
chartName: "non-existing",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cv, err := r.Get(tt.chartName, tt.chartVersion)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && !strings.Contains(cv.Metadata.Version, tt.wantVersion) {
t.Errorf("Get() unexpected version = %s, want = %s", cv.Metadata.Version, tt.wantVersion)
}
})
}
}
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 {
t.Run(tt.name, func(t *testing.T) {
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)
return
}
if err == nil && mg.requestedURL != tt.wantURL {
t.Errorf("DownloadChart() requested URL = %s, wantURL %s", mg.requestedURL, tt.wantURL)
}
})
}
}
func TestChartRepository_DownloadIndex(t *testing.T) {
b, err := ioutil.ReadFile(chartmuseumtestfile)
if err != nil {
t.Fatal(err)
}
mg := mockGetter{response: b}
r := &ChartRepository{
URL: "https://example.com",
Client: &mg,
}
if err := r.DownloadIndex(); 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)
}
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) {
tests := []struct {
name string
filename string
}{
{
name: "regular index file",
filename: testfile,
},
{
name: "chartmuseum index file",
filename: chartmuseumtestfile,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
b, err := ioutil.ReadFile(tt.filename)
if err != nil {
t.Fatal(err)
}
r := &ChartRepository{}
err = r.LoadIndex(b)
if err != nil {
t.Fatal(err)
}
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_LoadIndex_Unordered(t *testing.T) {
b, err := ioutil.ReadFile(unorderedtestfile)
if err != nil {
t.Fatal(err)
}
r := &ChartRepository{}
err = r.LoadIndex(b)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, r.Index)
}
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)
}
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)
}
nginx, ok := i.Entries["nginx"]
if !ok || len(nginx) != 2 {
t.Fatalf("Expected 2 nginx 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",
},
}
tests := []*repo.ChartVersion{alpine[0], nginx[0], nginx[1]}
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)
}
}
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
}