Construct http.Client for repositories from config, add TLS support

pull/1766/head
Anton Galitsyn 8 years ago committed by Anton Galitsyn
parent b928088a8a
commit b0e7a43b5b

@ -103,7 +103,7 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Fatal(err)
}
i, err := repo.LoadIndexFile(dbc.helmhome.CacheIndex("test"))
i, err := repo.NewChartRepositoryIndexFromFile(dbc.helmhome.CacheIndex("test"))
if err != nil {
t.Fatal(err)
}

@ -88,7 +88,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err)
}
i, err := repo.LoadIndexFile(duc.helmhome.CacheIndex("test"))
i, err := repo.NewChartRepositoryIndexFromFile(duc.helmhome.CacheIndex("test"))
if err != nil {
t.Fatal(err)
}

@ -166,7 +166,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
}
// Next, we need to load the index, and actually look up the chart.
i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(repoName))
i, err := repo.NewChartRepositoryIndexFromFile(c.HelmHome.CacheIndex(repoName))
if err != nil {
return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
}
@ -182,7 +182,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
return url.Parse(cv.URLs[0])
}
func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) {
func findRepoEntry(name string, repos []*repo.ChartRepositoryConfig) (*repo.ChartRepositoryConfig, error) {
for _, re := range repos {
if re.Name == name {
return re, nil

@ -35,6 +35,7 @@ import (
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/urlutil"
)
// Manager handles the lifecycle of fetching, resolving, and storing dependencies.
@ -226,7 +227,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
found = true
} else {
for _, repo := range repos {
if urlsAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
if urlutil.URLAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
found = true
}
}
@ -258,7 +259,7 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
found := false
for _, repo := range repos {
if urlsAreEqual(repo.URL, dd.Repository) {
if urlutil.URLAreEqual(repo.URL, dd.Repository) {
found = true
reposMap[dd.Name] = repo.Name
break
@ -283,53 +284,35 @@ func (m *Manager) UpdateRepositories() error {
repos := rf.Repositories
if len(repos) > 0 {
// This prints warnings straight to out.
m.parallelRepoUpdate(repos)
if err := m.parallelRepoUpdate(repos); err != nil {
return err
}
}
return nil
}
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) {
func (m *Manager) parallelRepoUpdate(repos []*repo.ChartRepositoryConfig) error {
out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
for _, re := range repos {
for _, c := range repos {
r, err := repo.NewChartRepository(c)
if err != nil {
return err
}
wg.Add(1)
go func(n, u string) {
if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
go func(r *repo.ChartRepository) {
if err := r.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n)
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
}
wg.Done()
}(re.Name, re.URL)
}(r)
}
wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
}
// urlsAreEqual normalizes two URLs and then compares for equality.
//
// TODO: This and the urlJoin functions should really be moved to a 'urlutil' package.
func urlsAreEqual(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
a = filepath.Clean(a)
b = filepath.Clean(b)
// If urls are paths, return true only if they are an exact match
return a == b
}
bu, err := url.Parse(b)
if err != nil {
return false
}
for _, u := range []*url.URL{au, bu} {
if u.Path == "" {
u.Path = "/"
}
u.Path = filepath.Clean(u.Path)
}
return au.String() == bu.String()
return nil
}
// findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
@ -342,7 +325,7 @@ func urlsAreEqual(a, b string) bool {
// If it finds a URL that is "relative", it will prepend the repoURL.
func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos {
if urlsAreEqual(repoURL, cr.URL) {
if urlutil.URLAreEqual(repoURL, cr.Config.URL) {
entry, err := findEntryByName(name, cr)
if err != nil {
return "", err
@ -434,13 +417,14 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
for _, re := range rf.Repositories {
lname := re.Name
cacheindex := m.HelmHome.CacheIndex(lname)
index, err := repo.LoadIndexFile(cacheindex)
index, err := repo.NewChartRepositoryIndexFromFile(cacheindex)
if err != nil {
return indices, err
}
// TODO: use constructor
cr := &repo.ChartRepository{
URL: re.URL,
Config: re,
IndexFile: index,
}
indices[lname] = cr

@ -135,27 +135,3 @@ func TestGetRepoNames(t *testing.T) {
}
}
}
func TestUrlsAreEqual(t *testing.T) {
for _, tt := range []struct {
a, b string
match bool
}{
{"http://example.com", "http://example.com", true},
{"http://example.com", "http://another.example.com", false},
{"https://example.com", "https://example.com", true},
{"http://example.com/", "http://example.com", true},
{"https://example.com", "http://example.com", false},
{"http://example.com/foo", "http://example.com/foo/", true},
{"http://example.com/foo//", "http://example.com/foo/", true},
{"http://example.com/./foo/", "http://example.com/foo/", true},
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
{"/foo", "/foo", true},
{"/foo", "/foo/", true},
{"/foo/.", "/foo/", true},
} {
if tt.match != urlsAreEqual(tt.a, tt.b) {
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
}
}
}

@ -92,7 +92,6 @@ func newFetchCmd(out io.Writer) *cobra.Command {
}
func (f *fetchCmd) run() error {
pname := f.chartRef
c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()),
Out: f.out,
@ -118,7 +117,7 @@ func (f *fetchCmd) run() error {
defer os.RemoveAll(dest)
}
saved, v, err := c.DownloadTo(pname, f.version, dest)
saved, v, err := c.DownloadTo(f.chartRef, f.version, dest)
if err != nil {
return err
}

@ -254,12 +254,12 @@ func ensureTestHome(home helmpath.Home, t *testing.T) error {
repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil {
rf := repo.NewRepoFile()
rf.Add(&repo.Entry{
rf := repo.NewRepositoryFile()
rf.Add(&repo.ChartRepositoryConfig{
Name: "charts",
URL: "http://example.com/foo",
Cache: "charts-index.yaml",
}, &repo.Entry{
}, &repo.ChartRepositoryConfig{
Name: "local",
URL: "http://localhost.com:7743/foo",
Cache: "local-index.yaml",
@ -279,7 +279,7 @@ func ensureTestHome(home helmpath.Home, t *testing.T) error {
localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath)
if fi, err := os.Stat(localRepoIndexFile); err != nil {
i := repo.NewIndexFile()
i := repo.NewChartRepositoryIndex()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err
}

@ -102,7 +102,6 @@ func newInitCmd(out io.Writer) *cobra.Command {
// runInit initializes local config and installs tiller to Kubernetes Cluster
func (i *initCmd) run() error {
if flagDebug {
m, err := installer.DeploymentManifest(i.namespace, i.image, i.canary)
if err != nil {
@ -110,13 +109,21 @@ func (i *initCmd) run() error {
}
fmt.Fprintln(i.out, m)
}
if i.dryRun {
return nil
}
if err := ensureHome(i.home, i.out); err != nil {
if err := ensureDirectories(i.home, i.out); err != nil {
return err
}
if err := ensureDefaultRepos(i.home, i.out); err != nil {
return err
}
if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil {
return err
}
fmt.Fprintf(i.out, "$HELM_HOME has been configured at %s.\n", helmHome)
if !i.clientOnly {
if i.kubeClient == nil {
@ -137,15 +144,23 @@ func (i *initCmd) run() error {
} else {
fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set")
}
fmt.Fprintln(i.out, "Happy Helming!")
return nil
}
// ensureHome checks to see if $HELM_HOME exists
// ensureDirectories checks to see if $HELM_HOME exists
//
// If $HELM_HOME does not exist, this function will create it.
func ensureHome(home helmpath.Home, out io.Writer) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()}
func ensureDirectories(home helmpath.Home, out io.Writer) error {
configDirectories := []string{
home.String(),
home.Repository(),
home.Cache(),
home.LocalRepository(),
home.Plugins(),
home.Starters(),
}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
fmt.Fprintf(out, "Creating %s \n", p)
@ -157,50 +172,77 @@ func ensureHome(home helmpath.Home, out io.Writer) error {
}
}
return nil
}
func ensureDefaultRepos(home helmpath.Home, out io.Writer) error {
repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", repoFile)
r := repo.NewRepoFile()
r.Add(&repo.Entry{
Name: stableRepository,
URL: stableRepositoryURL,
Cache: "stable-index.yaml",
}, &repo.Entry{
Name: localRepository,
URL: localRepositoryURL,
Cache: "local-index.yaml",
})
if err := r.WriteFile(repoFile, 0644); err != nil {
f := repo.NewRepositoryFile()
sr, err := initStableRepo(home.CacheIndex(stableRepository))
if err != nil {
return err
}
cif := home.CacheIndex(stableRepository)
if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil {
fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm repo update')\n", stableRepository, err)
lr, err := initLocalRepo(home.LocalRepository(localRepoIndexFilePath), home.CacheIndex("local"))
if err != nil {
return err
}
f.Add(sr)
f.Add(lr)
f.WriteFile(repoFile, 0644)
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", repoFile)
}
if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
return nil
}
func initStableRepo(cacheFile string) (*repo.ChartRepositoryConfig, error) {
c := repo.ChartRepositoryConfig{
Name: stableRepository,
URL: stableRepositoryURL,
Cache: cacheFile,
}
r, err := repo.NewChartRepository(&c)
if err != nil {
return nil, err
}
localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath)
if fi, err := os.Stat(localRepoIndexFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile)
i := repo.NewIndexFile()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err
if err := r.DownloadIndexFile(); err != nil {
return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error())
}
return &c, nil
}
func initLocalRepo(indexFile, cacheFile string) (*repo.ChartRepositoryConfig, error) {
if fi, err := os.Stat(indexFile); err != nil {
i := repo.NewChartRepositoryIndex()
if err := i.WriteFile(indexFile, 0644); err != nil {
return nil, err
}
//TODO: take this out and replace with helm update functionality
os.Symlink(localRepoIndexFile, home.CacheIndex("local"))
os.Symlink(indexFile, cacheFile)
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile)
return nil, fmt.Errorf("%s must be a file, not a directory", indexFile)
}
return &repo.ChartRepositoryConfig{
Name: localRepository,
URL: localRepositoryURL,
Cache: cacheFile,
}, nil
}
func ensureRepoFileFormat(file string, out io.Writer) error {
r, err := repo.LoadRepositoriesFile(file)
if err == repo.ErrRepoOutOfDate {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(file, 0644); err != nil {
return err
}
}
fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", helmHome)
return nil
}

@ -43,20 +43,22 @@ func TestInitCmd(t *testing.T) {
defer os.Remove(home)
var buf bytes.Buffer
fc := fake.NewSimpleClientset()
fake := testclient.Fake{}
cmd := &initCmd{
out: &buf,
home: helmpath.Home(home),
kubeClient: fc.Extensions(),
namespace: api.NamespaceDefault,
kubeClient: fake.Extensions(),
}
if err := cmd.run(); err != nil {
t.Errorf("expected error: %v", err)
}
action := fc.Actions()[0]
if !action.Matches("create", "deployments") {
t.Errorf("unexpected action: %v, expected create deployment", action)
actions := fake.Actions()
if action, ok := actions[0].(testclient.CreateAction); !ok || action.GetResource() != "deployments" {
t.Errorf("unexpected action: %v, expected create deployment", actions[0])
}
expected := "Tiller (the helm server side component) has been installed into your Kubernetes Cluster."
if !strings.Contains(buf.String(), expected) {
t.Errorf("expected %q, got %q", expected, buf.String())
@ -169,7 +171,13 @@ func TestEnsureHome(t *testing.T) {
b := bytes.NewBuffer(nil)
hh := helmpath.Home(home)
helmHome = home
if err := ensureHome(hh, b); err != nil {
if err := ensureDirectories(hh, b); err != nil {
t.Error(err)
}
if err := ensureDefaultRepos(hh, b); err != nil {
t.Error(err)
}
if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil {
t.Error(err)
}

@ -19,8 +19,6 @@ package main
import (
"fmt"
"io"
"path/filepath"
"strings"
"github.com/spf13/cobra"
@ -32,8 +30,13 @@ type repoAddCmd struct {
name string
url string
home helmpath.Home
out io.Writer
noupdate bool
certFile string
keyFile string
caFile string
out io.Writer
}
func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -56,73 +59,54 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
return add.run()
},
}
f := cmd.Flags()
f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered")
f.StringVar(&add.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&add.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&add.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
return cmd
}
func (a *repoAddCmd) run() error {
var err error
if a.noupdate {
err = addRepository(a.name, a.url, a.home)
} else {
err = updateRepository(a.name, a.url, a.home)
}
if err != nil {
if err := addRepository(a.name, a.url, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil {
return err
}
fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name)
return nil
}
func addRepository(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
if err := repo.DownloadIndexFile(name, url, cif); err != nil {
return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
}
return insertRepoLine(name, url, home)
}
func insertRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
func addRepository(name, url string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error {
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil {
return err
}
if f.Has(name) {
if noUpdate && f.Has(name) {
return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name)
}
f.Add(&repo.Entry{
Name: name,
URL: strings.TrimSuffix(url, "/"),
Cache: filepath.Base(cif),
})
return f.WriteFile(home.RepositoryFile(), 0644)
}
func updateRepository(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
if err := repo.DownloadIndexFile(name, url, cif); err != nil {
return err
c := repo.ChartRepositoryConfig{
Name: name,
Cache: cif,
URL: url,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
}
return updateRepoLine(name, url, home)
}
func updateRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
r, err := repo.NewChartRepository(&c)
if err != nil {
return err
}
f.Update(&repo.Entry{
Name: name,
URL: url,
Cache: filepath.Base(cif),
})
if err := r.DownloadIndexFile(); err != nil {
return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
}
f.Update(&c)
return f.WriteFile(home.RepositoryFile(), 0666)
return f.WriteFile(home.RepositoryFile(), 0644)
}

@ -80,7 +80,7 @@ func TestRepoAdd(t *testing.T) {
t.Fatal(err)
}
if err := addRepository(testName, ts.URL(), hh); err != nil {
if err := addRepository(testName, ts.URL(), hh, "", "", "", true); err != nil {
t.Error(err)
}
@ -93,11 +93,11 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile())
}
if err := updateRepository(testName, ts.URL(), hh); err != nil {
if err := addRepository(testName, ts.URL(), hh, "", "", "", false); err != nil {
t.Errorf("Repository was not updated: %s", err)
}
if err := addRepository(testName, ts.URL(), hh); err == nil {
if err := addRepository(testName, ts.URL(), hh, "", "", "", false); err != nil {
t.Errorf("Duplicate repository name was added")
}
}

@ -83,12 +83,12 @@ func (i *repoIndexCmd) run() error {
func index(dir, url, mergeTo string) error {
out := filepath.Join(dir, "index.yaml")
i, err := repo.IndexDirectory(dir, url)
i, err := repo.NewChartRepositoryIndexFromDirectory(dir, url)
if err != nil {
return err
}
if mergeTo != "" {
i2, err := repo.LoadIndexFile(mergeTo)
i2, err := repo.NewChartRepositoryIndexFromFile(mergeTo)
if err != nil {
return fmt.Errorf("Merge failed: %s", err)
}

@ -53,7 +53,7 @@ func TestRepoIndexCmd(t *testing.T) {
destIndex := filepath.Join(dir, "index.yaml")
index, err := repo.LoadIndexFile(destIndex)
index, err := repo.NewChartRepositoryIndexFromFile(destIndex)
if err != nil {
t.Fatal(err)
}
@ -94,7 +94,7 @@ func TestRepoIndexCmd(t *testing.T) {
t.Error(err)
}
index, err = repo.LoadIndexFile(destIndex)
index, err = repo.NewChartRepositoryIndexFromFile(destIndex)
if err != nil {
t.Fatal(err)
}

@ -24,24 +24,33 @@ import (
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
)
func TestRepoRemove(t *testing.T) {
testURL := "https://test-url.com"
b := bytes.NewBuffer(nil)
home, err := tempHelmHome(t)
ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer os.Remove(home)
hh := helmpath.Home(home)
oldhome := homePath()
helmHome = thome
hh := helmpath.Home(thome)
defer func() {
ts.Stop()
helmHome = oldhome
os.Remove(thome)
}()
if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err)
}
b := bytes.NewBuffer(nil)
if err := removeRepoLine(b, testName, hh); err == nil {
t.Errorf("Expected error removing %s, but did not get one.", testName)
}
if err := insertRepoLine(testName, testURL, hh); err != nil {
if err := addRepository(testName, ts.URL(), hh, "", "", "", true); err != nil {
t.Error(err)
}

@ -36,10 +36,14 @@ Information is cached locally, where it is used by commands like 'helm search'.
future releases.
`
var (
errNoRepositories = errors.New("no repositories found. You must add one before updating")
)
type repoUpdateCmd struct {
update func([]*repo.Entry, bool, io.Writer, helmpath.Home)
out io.Writer
update func([]*repo.ChartRepository, io.Writer)
home helmpath.Home
out io.Writer
}
func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -67,31 +71,39 @@ func (u *repoUpdateCmd) run() error {
}
if len(f.Repositories) == 0 {
return errors.New("no repositories found. You must add one before updating")
return errNoRepositories
}
var repos []*repo.ChartRepository
for _, cfg := range f.Repositories {
r, err := repo.NewChartRepository(cfg)
if err != nil {
return err
}
repos = append(repos, r)
}
u.update(f.Repositories, flagDebug, u.out, u.home)
u.update(repos, u.out)
return nil
}
func updateCharts(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) {
func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
for _, re := range repos {
wg.Add(1)
go func(n, u string) {
go func(re *repo.ChartRepository) {
defer wg.Done()
if n == localRepository {
// We skip local because the indices are symlinked.
if re.Config.Name == localRepository {
fmt.Fprintf(out, "...Skip %s chart repository", re.Config.Name)
return
}
err := repo.DownloadIndexFile(n, u, home.CacheIndex(n))
err := re.DownloadIndexFile()
if err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n)
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
}
}(re.Name, re.URL)
}(re)
}
wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ")

@ -43,15 +43,15 @@ func TestUpdateCmd(t *testing.T) {
out := bytes.NewBuffer(nil)
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) {
updater := func(repos []*repo.ChartRepository, out io.Writer) {
for _, re := range repos {
fmt.Fprintln(out, re.Name)
fmt.Fprintln(out, re.Config.Name)
}
}
uc := &repoUpdateCmd{
out: out,
update: updater,
home: helmpath.Home(thome),
out: out,
}
if err := uc.run(); err != nil {
t.Fatal(err)
@ -63,33 +63,40 @@ func TestUpdateCmd(t *testing.T) {
}
func TestUpdateCharts(t *testing.T) {
srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
oldhome := homePath()
helmHome = thome
hh := helmpath.Home(thome)
defer func() {
srv.Stop()
ts.Stop()
helmHome = oldhome
os.Remove(thome)
}()
if err := ensureTestHome(helmpath.Home(thome), t); err != nil {
if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err)
}
buf := bytes.NewBuffer(nil)
repos := []*repo.Entry{
{Name: "charts", URL: srv.URL()},
r, err := repo.NewChartRepository(&repo.ChartRepositoryConfig{
Name: "charts",
URL: ts.URL(),
Cache: hh.CacheIndex("charts"),
})
if err != nil {
t.Error(err)
}
updateCharts(repos, false, buf, helmpath.Home(thome))
got := buf.String()
b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b)
got := b.String()
if strings.Contains(got, "Unable to get an update") {
t.Errorf("Failed to get a repo: %q", got)
}
if !strings.Contains(got, "Update Complete.") {
t.Errorf("Update was not successful")
t.Error("Update was not successful")
}
}

@ -60,7 +60,7 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements, repoNames map[string]st
return nil, fmt.Errorf("dependency %q has an invalid version/constraint format: %s", d.Name, err)
}
repoIndex, err := repo.LoadIndexFile(r.helmhome.CacheIndex(repoNames[d.Name]))
repoIndex, err := repo.NewChartRepositoryIndexFromFile(r.helmhome.CacheIndex(repoNames[d.Name]))
if err != nil {
return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
}

@ -119,7 +119,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) {
for _, re := range rf.Repositories {
n := re.Name
f := s.helmhome.CacheIndex(n)
ind, err := repo.LoadIndexFile(f)
ind, err := repo.NewChartRepositoryIndexFromFile(f)
if err != nil {
fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
continue

@ -61,7 +61,7 @@ func NewIndex() *Index {
const verSep = "$$"
// AddRepo adds a repository index to the search index.
func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) {
func (i *Index) AddRepo(rname string, ind *repo.ChartRepositoryIndex, all bool) {
for name, ref := range ind.Entries {
if len(ref) == 0 {
// Skip chart names that have zero releases.

@ -95,8 +95,8 @@ var indexfileEntries = map[string]repo.ChartVersions{
func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
i.AddRepo("testing", &repo.ChartRepositoryIndex{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.ChartRepositoryIndex{Entries: map[string]repo.ChartVersions{
"pinta": {
{
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},

@ -0,0 +1,168 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 repo // import "k8s.io/helm/pkg/repo"
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/tlsutil"
)
// ChartRepositoryConfig represents a collection of parameters for chart repository
type ChartRepositoryConfig struct {
Name string `json:"name"`
Cache string `json:"cache"`
URL string `json:"url"`
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
CAFile string `json:"caFile"`
}
// ChartRepository represents a chart repository
type ChartRepository struct {
Config *ChartRepositoryConfig
ChartPaths []string
IndexFile *ChartRepositoryIndex
Client *http.Client
}
// NewChartRepository constructs ChartRepository
func NewChartRepository(cfg *ChartRepositoryConfig) (*ChartRepository, error) {
var client *http.Client
if cfg.CertFile != "" && cfg.KeyFile != "" && cfg.CAFile != "" {
tlsConf, err := tlsutil.NewClientTLS(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
if err != nil {
return nil, fmt.Errorf("can't create TLS config for client: %s", err.Error())
}
tlsConf.BuildNameToCertificate()
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConf,
},
}
} else {
client = http.DefaultClient
}
return &ChartRepository{
Config: cfg,
IndexFile: NewChartRepositoryIndex(),
Client: client,
}, nil
}
// Load loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
func (r *ChartRepository) Load() error {
dirInfo, err := os.Stat(r.Config.Name)
if err != nil {
return err
}
if !dirInfo.IsDir() {
return fmt.Errorf("%q is not a directory", r.Config.Name)
}
// FIXME: Why are we recursively walking directories?
// FIXME: Why are we not reading the repositories.yaml to figure out
// what repos to use?
filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := NewChartRepositoryIndexFromFile(path)
if err != nil {
return nil
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
r.ChartPaths = append(r.ChartPaths, path)
}
}
return nil
})
return nil
}
// DownloadIndexFile fetches the index from a repository.
func (r *ChartRepository) DownloadIndexFile() error {
var indexURL string
indexURL = strings.TrimSuffix(r.Config.URL, "/") + "/index.yaml"
resp, err := r.Client.Get(indexURL)
if err != nil {
return err
}
defer resp.Body.Close()
index, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if _, err := loadIndex(index); err != nil {
return err
}
return ioutil.WriteFile(r.Config.Cache, index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error {
err := r.generateIndex()
if err != nil {
return err
}
return r.saveIndexFile()
}
func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(r.IndexFile)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
}
func (r *ChartRepository) generateIndex() error {
for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path)
if err != nil {
return err
}
digest, err := provenance.DigestFile(path)
if err != nil {
return err
}
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
}
// TODO: If a chart exists, but has a different Digest, should we error?
}
r.IndexFile.SortEntries()
return nil
}

@ -0,0 +1,185 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 repo
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const (
testRepository = "testdata/repository"
testURL = "http://example-charts.com"
)
func TestLoadChartRepository(t *testing.T) {
r, err := NewChartRepository(&ChartRepositoryConfig{
Name: testRepository,
URL: testURL,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
}
if err := r.Load(); err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
paths := []string{
filepath.Join(testRepository, "frobnitz-1.2.3.tgz"),
filepath.Join(testRepository, "sprocket-1.1.0.tgz"),
filepath.Join(testRepository, "sprocket-1.2.0.tgz"),
}
if r.Config.Name != testRepository {
t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name)
}
if !reflect.DeepEqual(r.ChartPaths, paths) {
t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths)
}
if r.Config.URL != testURL {
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL)
}
}
func TestIndex(t *testing.T) {
r, err := NewChartRepository(&ChartRepositoryConfig{
Name: testRepository,
URL: testURL,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
}
if err := r.Load(); err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
err = r.Index()
if err != nil {
t.Errorf("Error performing index: %v\n", err)
}
tempIndexPath := filepath.Join(testRepository, indexPath)
actual, err := NewChartRepositoryIndexFromFile(tempIndexPath)
defer os.Remove(tempIndexPath) // clean up
if err != nil {
t.Errorf("Error loading index file %v", err)
}
verifyIndex(t, actual)
// Re-index and test again.
err = r.Index()
if err != nil {
t.Errorf("Error performing re-index: %s\n", err)
}
second, err := NewChartRepositoryIndexFromFile(tempIndexPath)
if err != nil {
t.Errorf("Error re-loading index file %v", err)
}
verifyIndex(t, second)
}
func verifyIndex(t *testing.T, actual *ChartRepositoryIndex) {
var empty time.Time
if actual.Generated == empty {
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
}
if actual.APIVersion != APIVersionV1 {
t.Error("Expected v1 API")
}
entries := actual.Entries
if numEntries := len(entries); numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
expects := map[string]ChartVersions{
"frobnitz": {
{
Metadata: &chart.Metadata{
Name: "frobnitz",
Version: "1.2.3",
},
},
},
"sprocket": {
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.2.0",
},
},
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.1.0",
},
},
},
}
for name, versions := range expects {
got, ok := entries[name]
if !ok {
t.Errorf("Could not find %q entry", name)
continue
}
if len(versions) != len(got) {
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
continue
}
for i, e := range versions {
g := got[i]
if e.Name != g.Name {
t.Errorf("Expected %q, got %q", e.Name, g.Name)
}
if e.Version != g.Version {
t.Errorf("Expected %q, got %q", e.Version, g.Version)
}
if len(g.Keywords) != 3 {
t.Error("Expected 3 keyrwords.")
}
if len(g.Maintainers) != 2 {
t.Error("Expected 2 maintainers.")
}
if g.Created == empty {
t.Error("Expected created to be non-empty")
}
if g.Description == "" {
t.Error("Expected description to be non-empty")
}
if g.Home == "" {
t.Error("Expected home to be non-empty")
}
if g.Digest == "" {
t.Error("Expected digest to be non-empty")
}
if len(g.URLs) != 1 {
t.Error("Expected exactly 1 URL")
}
}
}
}

@ -21,10 +21,7 @@ import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
@ -36,6 +33,7 @@ import (
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/urlutil"
)
var indexPath = "index.yaml"
@ -76,17 +74,35 @@ func (c ChartVersions) Less(a, b int) bool {
return i.LessThan(j)
}
// IndexFile represents the index file in a chart repository
type IndexFile struct {
// ChartRepositoryIndex represents the index file in a chart repository
type ChartRepositoryIndex struct {
APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"`
Entries map[string]ChartVersions `json:"entries"`
PublicKeys []string `json:"publicKeys,omitempty"`
}
// NewIndexFile initializes an index.
func NewIndexFile() *IndexFile {
return &IndexFile{
// ChartVersion represents a chart entry in the ChartRepositoryIndex
type ChartVersion struct {
*chart.Metadata
URLs []string `json:"urls"`
Created time.Time `json:"created,omitempty"`
Removed bool `json:"removed,omitempty"`
Digest string `json:"digest,omitempty"`
}
// unversionedEntry represents a deprecated pre-Alpha.5 format.
//
// This will be removed prior to v2.0.0
type unversionedEntry struct {
Checksum string `json:"checksum"`
URL string `json:"url"`
Chartfile *chart.Metadata `json:"chartfile"`
}
// NewChartRepositoryIndex initializes an index.
func NewChartRepositoryIndex() *ChartRepositoryIndex {
return &ChartRepositoryIndex{
APIVersion: APIVersionV1,
Generated: time.Now(),
Entries: map[string]ChartVersions{},
@ -94,14 +110,103 @@ func NewIndexFile() *IndexFile {
}
}
// NewChartRepositoryIndexFromFile takes a file at the given path and returns an ChartRepositoryIndex object
func NewChartRepositoryIndexFromFile(path string) (*ChartRepositoryIndex, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return loadIndex(b)
}
// NewChartRepositoryIndexFromDirectory reads a (flat) directory and generates an index.
//
// It indexes only charts that have been packaged (*.tgz).
//
// The index returned will be in an unsorted state
func NewChartRepositoryIndexFromDirectory(dir, baseURL string) (*ChartRepositoryIndex, error) {
archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
if err != nil {
return nil, err
}
index := NewChartRepositoryIndex()
for _, arch := range archives {
fname := filepath.Base(arch)
c, err := chartutil.Load(arch)
if err != nil {
// Assume this is not a chart.
continue
}
hash, err := provenance.DigestFile(arch)
if err != nil {
return index, err
}
index.Add(c.Metadata, fname, baseURL, hash)
}
return index, nil
}
// loadIndex loads an index file and does minimal validity checking.
//
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte) (*ChartRepositoryIndex, error) {
i := &ChartRepositoryIndex{}
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
}
if i.APIVersion == "" {
// When we leave Beta, we should remove legacy support and just
// return this error:
//return i, ErrNoAPIVersion
return loadUnversionedIndex(data)
}
return i, nil
}
// loadUnversionedIndex loads a pre-Alpha.5 index.yaml file.
//
// This format is deprecated. This function will be removed prior to v2.0.0.
func loadUnversionedIndex(data []byte) (*ChartRepositoryIndex, error) {
fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'")
i := map[string]unversionedEntry{}
// This gets around an error in the YAML parser. Instead of parsing as YAML,
// we convert to JSON, and then decode again.
var err error
data, err = yaml.YAMLToJSON(data)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &i); err != nil {
return nil, err
}
if len(i) == 0 {
return nil, ErrNoAPIVersion
}
ni := NewChartRepositoryIndex()
for n, item := range i {
if item.Chartfile == nil || item.Chartfile.Name == "" {
parts := strings.Split(n, "-")
ver := ""
if len(parts) > 1 {
ver = strings.TrimSuffix(parts[1], ".tgz")
}
item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver}
}
ni.Add(item.Chartfile, item.URL, "", item.Checksum)
}
return ni, nil
}
// Add adds a file to the index
// This can leave the index in an unsorted state
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
func (i ChartRepositoryIndex) Add(md *chart.Metadata, filename, baseURL, digest string) {
u := filename
if baseURL != "" {
var err error
_, file := filepath.Split(filename)
u, err = urlJoin(baseURL, file)
u, err = urlutil.URLJoin(baseURL, file)
if err != nil {
u = filepath.Join(baseURL, file)
}
@ -120,7 +225,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
}
// Has returns true if the index has an entry for a chart with the given name and exact version.
func (i IndexFile) Has(name, version string) bool {
func (i ChartRepositoryIndex) Has(name, version string) bool {
_, err := i.Get(name, version)
return err == nil
}
@ -131,7 +236,7 @@ func (i IndexFile) Has(name, version string) bool {
// the most recent release for every version is in the 0th slot in the
// Entries.ChartVersions array. That way, tooling can predict the newest
// version without needing to parse SemVers.
func (i IndexFile) SortEntries() {
func (i ChartRepositoryIndex) SortEntries() {
for _, versions := range i.Entries {
sort.Sort(sort.Reverse(versions))
}
@ -140,7 +245,7 @@ func (i IndexFile) SortEntries() {
// Get returns the ChartVersion for the given name.
//
// If version is empty, this will return the chart with the highest version.
func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
func (i ChartRepositoryIndex) Get(name, version string) (*ChartVersion, error) {
vs, ok := i.Entries[name]
if !ok {
return nil, ErrNoChartName
@ -163,7 +268,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
// WriteFile writes an index file to the given destination path.
//
// The mode on the file is set to 'mode'.
func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
func (i ChartRepositoryIndex) WriteFile(dest string, mode os.FileMode) error {
b, err := yaml.Marshal(i)
if err != nil {
return err
@ -179,7 +284,7 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
// In all other cases, the existing record is preserved.
//
// This can leave the index in an unsorted state
func (i *IndexFile) Merge(f *IndexFile) {
func (i *ChartRepositoryIndex) Merge(f *ChartRepositoryIndex) {
for _, cvs := range f.Entries {
for _, cv := range cvs {
if !i.Has(cv.Name, cv.Version) {
@ -189,153 +294,3 @@ func (i *IndexFile) Merge(f *IndexFile) {
}
}
}
// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2
// ChartVersion represents a chart entry in the IndexFile
type ChartVersion struct {
*chart.Metadata
URLs []string `json:"urls"`
Created time.Time `json:"created,omitempty"`
Removed bool `json:"removed,omitempty"`
Digest string `json:"digest,omitempty"`
}
// IndexDirectory reads a (flat) directory and generates an index.
//
// It indexes only charts that have been packaged (*.tgz).
//
// The index returned will be in an unsorted state
func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
if err != nil {
return nil, err
}
index := NewIndexFile()
for _, arch := range archives {
fname := filepath.Base(arch)
c, err := chartutil.Load(arch)
if err != nil {
// Assume this is not a chart.
continue
}
hash, err := provenance.DigestFile(arch)
if err != nil {
return index, err
}
index.Add(c.Metadata, fname, baseURL, hash)
}
return index, nil
}
// DownloadIndexFile fetches the index from a repository.
func DownloadIndexFile(repoName, url, indexFilePath string) error {
var indexURL string
indexURL = strings.TrimSuffix(url, "/") + "/index.yaml"
resp, err := http.Get(indexURL)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if _, err := LoadIndex(b); err != nil {
return err
}
return ioutil.WriteFile(indexFilePath, b, 0644)
}
// LoadIndex loads an index file and does minimal validity checking.
//
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func LoadIndex(data []byte) (*IndexFile, error) {
i := &IndexFile{}
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
}
if i.APIVersion == "" {
// When we leave Beta, we should remove legacy support and just
// return this error:
//return i, ErrNoAPIVersion
return loadUnversionedIndex(data)
}
return i, nil
}
// unversionedEntry represents a deprecated pre-Alpha.5 format.
//
// This will be removed prior to v2.0.0
type unversionedEntry struct {
Checksum string `json:"checksum"`
URL string `json:"url"`
Chartfile *chart.Metadata `json:"chartfile"`
}
// loadUnversionedIndex loads a pre-Alpha.5 index.yaml file.
//
// This format is deprecated. This function will be removed prior to v2.0.0.
func loadUnversionedIndex(data []byte) (*IndexFile, error) {
fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'")
i := map[string]unversionedEntry{}
// This gets around an error in the YAML parser. Instead of parsing as YAML,
// we convert to JSON, and then decode again.
var err error
data, err = yaml.YAMLToJSON(data)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &i); err != nil {
return nil, err
}
if len(i) == 0 {
return nil, ErrNoAPIVersion
}
ni := NewIndexFile()
for n, item := range i {
if item.Chartfile == nil || item.Chartfile.Name == "" {
parts := strings.Split(n, "-")
ver := ""
if len(parts) > 1 {
ver = strings.TrimSuffix(parts[1], ".tgz")
}
item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver}
}
ni.Add(item.Chartfile, item.URL, "", item.Checksum)
}
return ni, nil
}
// LoadIndexFile takes a file at the given path and returns an IndexFile object
func LoadIndexFile(path string) (*IndexFile, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return LoadIndex(b)
}
// urlJoin joins a base URL to one or more path components.
//
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
// perform a join.
//
// If the URL is unparsable, this returns an error.
func urlJoin(baseURL string, paths ...string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
// We want path instead of filepath because path always uses /.
all := []string{u.Path}
all = append(all, paths...)
u.Path = path.Join(all...)
return u.String(), nil
}

@ -33,7 +33,7 @@ const (
)
func TestIndexFile(t *testing.T) {
i := NewIndexFile()
i := NewChartRepositoryIndex()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
@ -67,7 +67,7 @@ func TestLoadIndex(t *testing.T) {
if err != nil {
t.Fatal(err)
}
i, err := LoadIndex(b)
i, err := loadIndex(b)
if err != nil {
t.Fatal(err)
}
@ -75,7 +75,7 @@ func TestLoadIndex(t *testing.T) {
}
func TestLoadIndexFile(t *testing.T) {
i, err := LoadIndexFile(testfile)
i, err := NewChartRepositoryIndexFromFile(testfile)
if err != nil {
t.Fatal(err)
}
@ -83,13 +83,13 @@ func TestLoadIndexFile(t *testing.T) {
}
func TestMerge(t *testing.T) {
ind1 := NewIndexFile()
ind1 := NewChartRepositoryIndex()
ind1.Add(&chart.Metadata{
Name: "dreadnought",
Version: "0.1.0",
}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa")
ind2 := NewIndexFile()
ind2 := NewChartRepositoryIndex()
ind2.Add(&chart.Metadata{
Name: "dreadnought",
Version: "0.2.0",
@ -132,21 +132,30 @@ func TestDownloadIndexFile(t *testing.T) {
}
defer os.RemoveAll(dirName)
path := filepath.Join(dirName, testRepo+"-index.yaml")
if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil {
indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml")
r, err := NewChartRepository(&ChartRepositoryConfig{
Name: testRepo,
URL: srv.URL,
Cache: indexFilePath,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
}
if err := r.DownloadIndexFile(); err != nil {
t.Errorf("%#v", err)
}
if _, err := os.Stat(path); err != nil {
if _, err := os.Stat(indexFilePath); err != nil {
t.Errorf("error finding created index file: %#v", err)
}
b, err := ioutil.ReadFile(path)
b, err := ioutil.ReadFile(indexFilePath)
if err != nil {
t.Errorf("error reading index file: %#v", err)
}
i, err := LoadIndex(b)
i, err := loadIndex(b)
if err != nil {
t.Errorf("Index %q failed to parse: %s", testfile, err)
return
@ -155,7 +164,7 @@ func TestDownloadIndexFile(t *testing.T) {
verifyLocalIndex(t, i)
}
func verifyLocalIndex(t *testing.T, i *IndexFile) {
func verifyLocalIndex(t *testing.T, i *ChartRepositoryIndex) {
numEntries := len(i.Entries)
if numEntries != 2 {
t.Errorf("Expected 2 entries in index file but got %d", numEntries)
@ -255,7 +264,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
func TestIndexDirectory(t *testing.T) {
dir := "testdata/repository"
index, err := IndexDirectory(dir, "http://localhost:8080")
index, err := NewChartRepositoryIndexFromDirectory(dir, "http://localhost:8080")
if err != nil {
t.Fatal(err)
}
@ -305,8 +314,7 @@ func TestLoadUnversionedIndex(t *testing.T) {
}
func TestIndexAdd(t *testing.T) {
i := NewIndexFile()
i := NewChartRepositoryIndex()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" {
@ -325,24 +333,3 @@ func TestIndexAdd(t *testing.T) {
t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0])
}
}
func TestUrlJoin(t *testing.T) {
tests := []struct {
name, url, expect string
paths []string
}{
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
}
for _, tt := range tests {
if got, err := urlJoin(tt.url, tt.paths...); err != nil {
t.Errorf("%s: error %q", tt.name, err)
} else if got != tt.expect {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
}
}
}

@ -82,7 +82,7 @@ func (s *RepositoryServer) htmlIndex(w http.ResponseWriter, r *http.Request) {
t := htemplate.Must(htemplate.New("index.html").Parse(indexHTMLTemplate))
// load index
lrp := filepath.Join(s.RepoPath, "index.yaml")
i, err := LoadIndexFile(lrp)
i, err := NewChartRepositoryIndexFromFile(lrp)
if err != nil {
http.Error(w, err.Error(), 500)
return
@ -107,7 +107,7 @@ func AddChartToLocalRepo(ch *chart.Chart, path string) error {
// Reindex adds an entry to the index file at the given path
func Reindex(ch *chart.Chart, path string) error {
name := ch.Metadata.Name + "-" + ch.Metadata.Version
y, err := LoadIndexFile(path)
y, err := NewChartRepositoryIndexFromFile(path)
if err != nil {
return err
}

@ -21,64 +21,44 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
)
// ErrRepoOutOfDate indicates that the repository file is out of date, but
// is fixable.
var ErrRepoOutOfDate = errors.New("repository file is out of date")
// ChartRepository represents a chart repository
type ChartRepository struct {
RootPath string
URL string // URL of repository
ChartPaths []string
IndexFile *IndexFile
}
// Entry represents one repo entry in a repositories listing.
type Entry struct {
Name string `json:"name"`
Cache string `json:"cache"`
URL string `json:"url"`
}
// RepoFile represents the repositories.yaml file in $HELM_HOME
type RepoFile struct {
APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"`
Repositories []*Entry `json:"repositories"`
// RepositoryFile represents the repositories.yaml file in $HELM_HOME
type RepositoryFile struct {
APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"`
Repositories []*ChartRepositoryConfig `json:"repositories"`
}
// NewRepoFile generates an empty repositories file.
// NewRepositoryFile generates an empty repositories file.
//
// Generated and APIVersion are automatically set.
func NewRepoFile() *RepoFile {
return &RepoFile{
func NewRepositoryFile() *RepositoryFile {
return &RepositoryFile{
APIVersion: APIVersionV1,
Generated: time.Now(),
Repositories: []*Entry{},
Repositories: []*ChartRepositoryConfig{},
}
}
// LoadRepositoriesFile takes a file at the given path and returns a RepoFile object
// LoadRepositoriesFile takes a file at the given path and returns a RepositoryFile object
//
// If this returns ErrRepoOutOfDate, it also returns a recovered RepoFile that
// If this returns ErrRepoOutOfDate, it also returns a recovered RepositoryFile that
// can be saved as a replacement to the out of date file.
func LoadRepositoriesFile(path string) (*RepoFile, error) {
func LoadRepositoriesFile(path string) (*RepositoryFile, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
r := &RepoFile{}
r := &RepositoryFile{}
err = yaml.Unmarshal(b, r)
if err != nil {
return nil, err
@ -90,9 +70,9 @@ func LoadRepositoriesFile(path string) (*RepoFile, error) {
if err = yaml.Unmarshal(b, &m); err != nil {
return nil, err
}
r := NewRepoFile()
r := NewRepositoryFile()
for k, v := range m {
r.Add(&Entry{
r.Add(&ChartRepositoryConfig{
Name: k,
URL: v,
Cache: fmt.Sprintf("%s-index.yaml", k),
@ -105,13 +85,13 @@ func LoadRepositoriesFile(path string) (*RepoFile, error) {
}
// Add adds one or more repo entries to a repo file.
func (r *RepoFile) Add(re ...*Entry) {
func (r *RepositoryFile) Add(re ...*ChartRepositoryConfig) {
r.Repositories = append(r.Repositories, re...)
}
// Update attempts to replace one or more repo entries in a repo file. If an
// entry with the same name doesn't exist in the repo file it will add it.
func (r *RepoFile) Update(re ...*Entry) {
func (r *RepositoryFile) Update(re ...*ChartRepositoryConfig) {
for _, target := range re {
found := false
for j, repo := range r.Repositories {
@ -128,7 +108,7 @@ func (r *RepoFile) Update(re ...*Entry) {
}
// Has returns true if the given name is already a repository name.
func (r *RepoFile) Has(name string) bool {
func (r *RepositoryFile) Has(name string) bool {
for _, rf := range r.Repositories {
if rf.Name == name {
return true
@ -138,8 +118,8 @@ func (r *RepoFile) Has(name string) bool {
}
// Remove removes the entry from the list of repositories.
func (r *RepoFile) Remove(name string) bool {
cp := []*Entry{}
func (r *RepositoryFile) Remove(name string) bool {
cp := []*ChartRepositoryConfig{}
found := false
for _, rf := range r.Repositories {
if rf.Name == name {
@ -153,90 +133,10 @@ func (r *RepoFile) Remove(name string) bool {
}
// WriteFile writes a repositories file to the given path.
func (r *RepoFile) WriteFile(path string, perm os.FileMode) error {
func (r *RepositoryFile) WriteFile(path string, perm os.FileMode) error {
data, err := yaml.Marshal(r)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, perm)
}
// LoadChartRepository loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
//
// This function evaluates the contents of the directory and
// returns a ChartRepository
func LoadChartRepository(dir, url string) (*ChartRepository, error) {
dirInfo, err := os.Stat(dir)
if err != nil {
return nil, err
}
if !dirInfo.IsDir() {
return nil, fmt.Errorf("%q is not a directory", dir)
}
r := &ChartRepository{RootPath: dir, URL: url}
// FIXME: Why are we recursively walking directories?
// FIXME: Why are we not reading the repositories.yaml to figure out
// what repos to use?
filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
r.ChartPaths = append(r.ChartPaths, path)
}
}
return nil
})
return r, nil
}
func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(r.IndexFile)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error {
err := r.generateIndex()
if err != nil {
return err
}
return r.saveIndexFile()
}
func (r *ChartRepository) generateIndex() error {
if r.IndexFile == nil {
r.IndexFile = NewIndexFile()
}
for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path)
if err != nil {
return err
}
digest, err := provenance.DigestFile(path)
if err != nil {
return err
}
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.URL, digest)
}
// TODO: If a chart exists, but has a different Digest, should we error?
}
r.IndexFile.SortEntries()
return nil
}

@ -16,29 +16,19 @@ limitations under the License.
package repo
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
import "testing"
const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository"
const testURL = "http://example-charts.com"
func TestRepoFile(t *testing.T) {
rf := NewRepoFile()
rf := NewRepositoryFile()
rf.Add(
&Entry{
&ChartRepositoryConfig{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
&ChartRepositoryConfig{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
@ -68,15 +58,15 @@ func TestRepoFile(t *testing.T) {
}
}
func TestLoadRepositoriesFile(t *testing.T) {
expects := NewRepoFile()
func TestNewRepositoriesFile(t *testing.T) {
expects := NewRepositoryFile()
expects.Add(
&Entry{
&ChartRepositoryConfig{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
&ChartRepositoryConfig{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
@ -106,7 +96,7 @@ func TestLoadRepositoriesFile(t *testing.T) {
}
}
func TestLoadPreV1RepositoriesFile(t *testing.T) {
func TestNewPreV1RepositoriesFile(t *testing.T) {
r, err := LoadRepositoriesFile("testdata/old-repositories.yaml")
if err != nil && err != ErrRepoOutOfDate {
t.Fatal(err)
@ -126,139 +116,3 @@ func TestLoadPreV1RepositoriesFile(t *testing.T) {
t.Errorf("expected the best charts ever. Got %#v", r.Repositories)
}
}
func TestLoadChartRepository(t *testing.T) {
cr, err := LoadChartRepository(testRepository, testURL)
if err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
paths := []string{filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), filepath.Join(testRepository, "sprocket-1.1.0.tgz"), filepath.Join(testRepository, "sprocket-1.2.0.tgz")}
if cr.RootPath != testRepository {
t.Errorf("Expected %s as RootPath but got %s", testRepository, cr.RootPath)
}
if !reflect.DeepEqual(cr.ChartPaths, paths) {
t.Errorf("Expected %#v but got %#v\n", paths, cr.ChartPaths)
}
if cr.URL != testURL {
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, cr.URL)
}
}
func TestIndex(t *testing.T) {
cr, err := LoadChartRepository(testRepository, testURL)
if err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
err = cr.Index()
if err != nil {
t.Errorf("Error performing index: %v\n", err)
}
tempIndexPath := filepath.Join(testRepository, indexPath)
actual, err := LoadIndexFile(tempIndexPath)
defer os.Remove(tempIndexPath) // clean up
if err != nil {
t.Errorf("Error loading index file %v", err)
}
verifyIndex(t, actual)
// Re-index and test again.
err = cr.Index()
if err != nil {
t.Errorf("Error performing re-index: %s\n", err)
}
second, err := LoadIndexFile(tempIndexPath)
if err != nil {
t.Errorf("Error re-loading index file %v", err)
}
verifyIndex(t, second)
}
func verifyIndex(t *testing.T, actual *IndexFile) {
var empty time.Time
if actual.Generated == empty {
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
}
if actual.APIVersion != APIVersionV1 {
t.Error("Expected v1 API")
}
entries := actual.Entries
if numEntries := len(entries); numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
expects := map[string]ChartVersions{
"frobnitz": {
{
Metadata: &chart.Metadata{
Name: "frobnitz",
Version: "1.2.3",
},
},
},
"sprocket": {
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.2.0",
},
},
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.1.0",
},
},
},
}
for name, versions := range expects {
got, ok := entries[name]
if !ok {
t.Errorf("Could not find %q entry", name)
continue
}
if len(versions) != len(got) {
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
continue
}
for i, e := range versions {
g := got[i]
if e.Name != g.Name {
t.Errorf("Expected %q, got %q", e.Name, g.Name)
}
if e.Version != g.Version {
t.Errorf("Expected %q, got %q", e.Version, g.Version)
}
if len(g.Keywords) != 3 {
t.Error("Expected 3 keyrwords.")
}
if len(g.Maintainers) != 2 {
t.Error("Expected 2 maintainers.")
}
if g.Created == empty {
t.Error("Expected created to be non-empty")
}
if g.Description == "" {
t.Error("Expected description to be non-empty")
}
if g.Home == "" {
t.Error("Expected home to be non-empty")
}
if g.Digest == "" {
t.Error("Expected digest to be non-empty")
}
if len(g.URLs) != 1 {
t.Error("Expected exactly 1 URL")
}
}
}
}

@ -24,6 +24,7 @@ import (
"github.com/ghodss/yaml"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
)
@ -69,7 +70,7 @@ func NewServer(docroot string) *Server {
}
srv.start()
// Add the testing repository as the only repo.
if err := setTestingRepository(docroot, "test", srv.URL()); err != nil {
if err := setTestingRepository(helmpath.Home(docroot), "test", srv.URL()); err != nil {
panic(err)
}
return srv
@ -113,7 +114,7 @@ func (s *Server) CopyCharts(origin string) ([]string, error) {
// CreateIndex will read docroot and generate an index.yaml file.
func (s *Server) CreateIndex() error {
// generate the index
index, err := repo.IndexDirectory(s.docroot, s.URL())
index, err := repo.NewChartRepositoryIndexFromDirectory(s.docroot, s.URL())
if err != nil {
return err
}
@ -158,11 +159,13 @@ func (s *Server) LinkIndices() error {
}
// setTestingRepository sets up a testing repository.yaml with only the given name/URL.
func setTestingRepository(helmhome, name, url string) error {
rf := repo.NewRepoFile()
rf.Add(&repo.Entry{Name: name, URL: url})
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
dest := filepath.Join(helmhome, "repository/repositories.yaml")
return rf.WriteFile(dest, 0644)
func setTestingRepository(home helmpath.Home, name, url string) error {
r := repo.NewRepositoryFile()
r.Add(&repo.ChartRepositoryConfig{
Name: name,
URL: url,
Cache: home.CacheIndex(name),
})
os.MkdirAll(filepath.Join(home.Repository(), name), 0755)
return r.WriteFile(home.RepositoryFile(), 0644)
}

@ -77,7 +77,7 @@ func TestServer(t *testing.T) {
return
}
m := repo.NewIndexFile()
m := repo.NewChartRepositoryIndex()
if err := yaml.Unmarshal(data, m); err != nil {
t.Error(err)
return

@ -0,0 +1,68 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 tlsutil
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)
// NewClientTLS returns tls.Config appropriate for client auth.
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
cert, err := CertFromFilePair(certFile, keyFile)
if err != nil {
return nil, err
}
cp, err := CertPoolFromFile(caFile)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{*cert},
RootCAs: cp,
}, nil
}
// CertPoolFromFile returns an x509.CertPool containing the certificates
// in the given PEM-encoded file.
// Returns an error if the file could not be read, a certificate could not
// be parsed, or if the file does not contain any certificates
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("can't read CA file: %v", filename)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("failed to append certificates from file: %s", filename)
}
return cp, nil
}
// CertFromFilePair returns an tls.Certificate containing the
// certificates public/private key pair from a pair of given PEM-encoded files.
// Returns an error if the file could not be read, a certificate could not
// be parsed, or if the file does not contain any certificates
func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("can't load key pair from cert %s and key %s", certFile, keyFile)
}
return &cert, err
}

@ -0,0 +1,66 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 urlutil
import (
"net/url"
"path"
"path/filepath"
)
// URLJoin joins a base URL to one or more path components.
//
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
// perform a join.
//
// If the URL is unparsable, this returns an error.
func URLJoin(baseURL string, paths ...string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
// We want path instead of filepath because path always uses /.
all := []string{u.Path}
all = append(all, paths...)
u.Path = path.Join(all...)
return u.String(), nil
}
// URLAreEqual normalizes two URLs and then compares for equality.
//
// TODO: This and the urlJoin functions should really be moved to a 'urlutil' package.
func URLAreEqual(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
a = filepath.Clean(a)
b = filepath.Clean(b)
// If urls are paths, return true only if they are an exact match
return a == b
}
bu, err := url.Parse(b)
if err != nil {
return false
}
for _, u := range []*url.URL{au, bu} {
if u.Path == "" {
u.Path = "/"
}
u.Path = filepath.Clean(u.Path)
}
return au.String() == bu.String()
}

@ -0,0 +1,64 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 urlutil
import "testing"
func TestUrlJoin(t *testing.T) {
tests := []struct {
name, url, expect string
paths []string
}{
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
}
for _, tt := range tests {
if got, err := URLJoin(tt.url, tt.paths...); err != nil {
t.Errorf("%s: error %q", tt.name, err)
} else if got != tt.expect {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
}
}
}
func TestUrlAreEqual(t *testing.T) {
for _, tt := range []struct {
a, b string
match bool
}{
{"http://example.com", "http://example.com", true},
{"http://example.com", "http://another.example.com", false},
{"https://example.com", "https://example.com", true},
{"http://example.com/", "http://example.com", true},
{"https://example.com", "http://example.com", false},
{"http://example.com/foo", "http://example.com/foo/", true},
{"http://example.com/foo//", "http://example.com/foo/", true},
{"http://example.com/./foo/", "http://example.com/foo/", true},
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
{"/foo", "/foo", true},
{"/foo", "/foo/", true},
{"/foo/.", "/foo/", true},
} {
if tt.match != URLAreEqual(tt.a, tt.b) {
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
}
}
}
Loading…
Cancel
Save