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

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

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

@ -88,7 +88,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
i, err := repo.LoadIndexFile(duc.helmhome.CacheIndex("test")) i, err := repo.NewChartRepositoryIndexFromFile(duc.helmhome.CacheIndex("test"))
if err != nil { if err != nil {
t.Fatal(err) 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. // 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 { if err != nil {
return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 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]) 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 { for _, re := range repos {
if re.Name == name { if re.Name == name {
return re, nil return re, nil

@ -35,6 +35,7 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/urlutil"
) )
// Manager handles the lifecycle of fetching, resolving, and storing dependencies. // Manager handles the lifecycle of fetching, resolving, and storing dependencies.
@ -226,7 +227,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
found = true found = true
} else { } else {
for _, repo := range repos { for _, repo := range repos {
if urlsAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { if urlutil.URLAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
found = true found = true
} }
} }
@ -258,7 +259,7 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
found := false found := false
for _, repo := range repos { for _, repo := range repos {
if urlsAreEqual(repo.URL, dd.Repository) { if urlutil.URLAreEqual(repo.URL, dd.Repository) {
found = true found = true
reposMap[dd.Name] = repo.Name reposMap[dd.Name] = repo.Name
break break
@ -283,53 +284,35 @@ func (m *Manager) UpdateRepositories() error {
repos := rf.Repositories repos := rf.Repositories
if len(repos) > 0 { if len(repos) > 0 {
// This prints warnings straight to out. // This prints warnings straight to out.
m.parallelRepoUpdate(repos) if err := m.parallelRepoUpdate(repos); err != nil {
return err
}
} }
return nil return nil
} }
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) { func (m *Manager) parallelRepoUpdate(repos []*repo.ChartRepositoryConfig) error {
out := m.Out out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup 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) wg.Add(1)
go func(n, u string) { go func(r *repo.ChartRepository) {
if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil { if err := r.DownloadIndexFile(); 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", r.Config.Name, r.Config.URL, err)
} else { } 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() wg.Done()
}(re.Name, re.URL) }(r)
} }
wg.Wait() wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
} return nil
// 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()
} }
// findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. // 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. // 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) { func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos { for _, cr := range repos {
if urlsAreEqual(repoURL, cr.URL) { if urlutil.URLAreEqual(repoURL, cr.Config.URL) {
entry, err := findEntryByName(name, cr) entry, err := findEntryByName(name, cr)
if err != nil { if err != nil {
return "", err return "", err
@ -434,13 +417,14 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
for _, re := range rf.Repositories { for _, re := range rf.Repositories {
lname := re.Name lname := re.Name
cacheindex := m.HelmHome.CacheIndex(lname) cacheindex := m.HelmHome.CacheIndex(lname)
index, err := repo.LoadIndexFile(cacheindex) index, err := repo.NewChartRepositoryIndexFromFile(cacheindex)
if err != nil { if err != nil {
return indices, err return indices, err
} }
// TODO: use constructor
cr := &repo.ChartRepository{ cr := &repo.ChartRepository{
URL: re.URL, Config: re,
IndexFile: index, IndexFile: index,
} }
indices[lname] = cr 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 { func (f *fetchCmd) run() error {
pname := f.chartRef
c := downloader.ChartDownloader{ c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()), HelmHome: helmpath.Home(homePath()),
Out: f.out, Out: f.out,
@ -118,7 +117,7 @@ func (f *fetchCmd) run() error {
defer os.RemoveAll(dest) 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 { if err != nil {
return err return err
} }

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

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

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

@ -19,8 +19,6 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -32,8 +30,13 @@ type repoAddCmd struct {
name string name string
url string url string
home helmpath.Home home helmpath.Home
out io.Writer
noupdate bool noupdate bool
certFile string
keyFile string
caFile string
out io.Writer
} }
func newRepoAddCmd(out io.Writer) *cobra.Command { func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -56,73 +59,54 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
return add.run() return add.run()
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered") 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 return cmd
} }
func (a *repoAddCmd) run() error { func (a *repoAddCmd) run() error {
var err error if err := addRepository(a.name, a.url, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil {
if a.noupdate {
err = addRepository(a.name, a.url, a.home)
} else {
err = updateRepository(a.name, a.url, a.home)
}
if err != nil {
return err return err
} }
fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name)
return nil return nil
} }
func addRepository(name, url string, home helmpath.Home) error { func addRepository(name, url string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) 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)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil { if err != nil {
return err 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) 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) cif := home.CacheIndex(name)
if err := repo.DownloadIndexFile(name, url, cif); err != nil { c := repo.ChartRepositoryConfig{
return err Name: name,
Cache: cif,
URL: url,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
} }
return updateRepoLine(name, url, home) r, err := repo.NewChartRepository(&c)
}
func updateRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil { if err != nil {
return err return err
} }
f.Update(&repo.Entry{ if err := r.DownloadIndexFile(); err != nil {
Name: name, return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
URL: url, }
Cache: filepath.Base(cif),
}) 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) 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) t.Error(err)
} }
@ -93,11 +93,11 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) 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) 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") t.Errorf("Duplicate repository name was added")
} }
} }

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

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

@ -24,24 +24,33 @@ import (
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
) )
func TestRepoRemove(t *testing.T) { func TestRepoRemove(t *testing.T) {
testURL := "https://test-url.com" ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
b := bytes.NewBuffer(nil)
home, err := tempHelmHome(t)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err := removeRepoLine(b, testName, hh); err == nil {
t.Errorf("Expected error removing %s, but did not get one.", testName) 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) t.Error(err)
} }

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

@ -43,15 +43,15 @@ func TestUpdateCmd(t *testing.T) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // 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 { for _, re := range repos {
fmt.Fprintln(out, re.Name) fmt.Fprintln(out, re.Config.Name)
} }
} }
uc := &repoUpdateCmd{ uc := &repoUpdateCmd{
out: out,
update: updater, update: updater,
home: helmpath.Home(thome), home: helmpath.Home(thome),
out: out,
} }
if err := uc.run(); err != nil { if err := uc.run(); err != nil {
t.Fatal(err) t.Fatal(err)
@ -63,33 +63,40 @@ func TestUpdateCmd(t *testing.T) {
} }
func TestUpdateCharts(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
oldhome := homePath() oldhome := homePath()
helmHome = thome helmHome = thome
hh := helmpath.Home(thome)
defer func() { defer func() {
srv.Stop() ts.Stop()
helmHome = oldhome helmHome = oldhome
os.Remove(thome) os.Remove(thome)
}() }()
if err := ensureTestHome(helmpath.Home(thome), t); err != nil { if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err) t.Fatal(err)
} }
buf := bytes.NewBuffer(nil) r, err := repo.NewChartRepository(&repo.ChartRepositoryConfig{
repos := []*repo.Entry{ Name: "charts",
{Name: "charts", URL: srv.URL()}, 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") { if strings.Contains(got, "Unable to get an update") {
t.Errorf("Failed to get a repo: %q", got) t.Errorf("Failed to get a repo: %q", got)
} }
if !strings.Contains(got, "Update Complete.") { 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) 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 { if err != nil {
return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) 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 { for _, re := range rf.Repositories {
n := re.Name n := re.Name
f := s.helmhome.CacheIndex(n) f := s.helmhome.CacheIndex(n)
ind, err := repo.LoadIndexFile(f) ind, err := repo.NewChartRepositoryIndexFromFile(f)
if err != nil { if err != nil {
fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
continue continue

@ -61,7 +61,7 @@ func NewIndex() *Index {
const verSep = "$$" const verSep = "$$"
// AddRepo adds a repository index to the search index. // 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 { for name, ref := range ind.Entries {
if len(ref) == 0 { if len(ref) == 0 {
// Skip chart names that have zero releases. // 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 { func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex() i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) i.AddRepo("testing", &repo.ChartRepositoryIndex{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ i.AddRepo("ztesting", &repo.ChartRepositoryIndex{Entries: map[string]repo.ChartVersions{
"pinta": { "pinta": {
{ {
URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, 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" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -36,6 +33,7 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/urlutil"
) )
var indexPath = "index.yaml" var indexPath = "index.yaml"
@ -76,17 +74,35 @@ func (c ChartVersions) Less(a, b int) bool {
return i.LessThan(j) return i.LessThan(j)
} }
// IndexFile represents the index file in a chart repository // ChartRepositoryIndex represents the index file in a chart repository
type IndexFile struct { type ChartRepositoryIndex struct {
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"` Generated time.Time `json:"generated"`
Entries map[string]ChartVersions `json:"entries"` Entries map[string]ChartVersions `json:"entries"`
PublicKeys []string `json:"publicKeys,omitempty"` PublicKeys []string `json:"publicKeys,omitempty"`
} }
// NewIndexFile initializes an index. // ChartVersion represents a chart entry in the ChartRepositoryIndex
func NewIndexFile() *IndexFile { type ChartVersion struct {
return &IndexFile{ *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, APIVersion: APIVersionV1,
Generated: time.Now(), Generated: time.Now(),
Entries: map[string]ChartVersions{}, 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 // Add adds a file to the index
// This can leave the index in an unsorted state // 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 u := filename
if baseURL != "" { if baseURL != "" {
var err error var err error
_, file := filepath.Split(filename) _, file := filepath.Split(filename)
u, err = urlJoin(baseURL, file) u, err = urlutil.URLJoin(baseURL, file)
if err != nil { if err != nil {
u = filepath.Join(baseURL, file) 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. // 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) _, err := i.Get(name, version)
return err == nil 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 // the most recent release for every version is in the 0th slot in the
// Entries.ChartVersions array. That way, tooling can predict the newest // Entries.ChartVersions array. That way, tooling can predict the newest
// version without needing to parse SemVers. // version without needing to parse SemVers.
func (i IndexFile) SortEntries() { func (i ChartRepositoryIndex) SortEntries() {
for _, versions := range i.Entries { for _, versions := range i.Entries {
sort.Sort(sort.Reverse(versions)) sort.Sort(sort.Reverse(versions))
} }
@ -140,7 +245,7 @@ func (i IndexFile) SortEntries() {
// Get returns the ChartVersion for the given name. // Get returns the ChartVersion for the given name.
// //
// If version is empty, this will return the chart with the highest version. // 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] vs, ok := i.Entries[name]
if !ok { if !ok {
return nil, ErrNoChartName 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. // WriteFile writes an index file to the given destination path.
// //
// The mode on the file is set to 'mode'. // 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) b, err := yaml.Marshal(i)
if err != nil { if err != nil {
return err 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. // In all other cases, the existing record is preserved.
// //
// This can leave the index in an unsorted state // 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 _, cvs := range f.Entries {
for _, cv := range cvs { for _, cv := range cvs {
if !i.Has(cv.Name, cv.Version) { 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) { 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: "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.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") 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
i, err := LoadIndex(b) i, err := loadIndex(b)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -75,7 +75,7 @@ func TestLoadIndex(t *testing.T) {
} }
func TestLoadIndexFile(t *testing.T) { func TestLoadIndexFile(t *testing.T) {
i, err := LoadIndexFile(testfile) i, err := NewChartRepositoryIndexFromFile(testfile)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -83,13 +83,13 @@ func TestLoadIndexFile(t *testing.T) {
} }
func TestMerge(t *testing.T) { func TestMerge(t *testing.T) {
ind1 := NewIndexFile() ind1 := NewChartRepositoryIndex()
ind1.Add(&chart.Metadata{ ind1.Add(&chart.Metadata{
Name: "dreadnought", Name: "dreadnought",
Version: "0.1.0", Version: "0.1.0",
}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa") }, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa")
ind2 := NewIndexFile() ind2 := NewChartRepositoryIndex()
ind2.Add(&chart.Metadata{ ind2.Add(&chart.Metadata{
Name: "dreadnought", Name: "dreadnought",
Version: "0.2.0", Version: "0.2.0",
@ -132,21 +132,30 @@ func TestDownloadIndexFile(t *testing.T) {
} }
defer os.RemoveAll(dirName) defer os.RemoveAll(dirName)
path := filepath.Join(dirName, testRepo+"-index.yaml") indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml")
if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil { 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) 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) t.Errorf("error finding created index file: %#v", err)
} }
b, err := ioutil.ReadFile(path) b, err := ioutil.ReadFile(indexFilePath)
if err != nil { if err != nil {
t.Errorf("error reading index file: %#v", err) t.Errorf("error reading index file: %#v", err)
} }
i, err := LoadIndex(b) i, err := loadIndex(b)
if err != nil { if err != nil {
t.Errorf("Index %q failed to parse: %s", testfile, err) t.Errorf("Index %q failed to parse: %s", testfile, err)
return return
@ -155,7 +164,7 @@ func TestDownloadIndexFile(t *testing.T) {
verifyLocalIndex(t, i) verifyLocalIndex(t, i)
} }
func verifyLocalIndex(t *testing.T, i *IndexFile) { func verifyLocalIndex(t *testing.T, i *ChartRepositoryIndex) {
numEntries := len(i.Entries) numEntries := len(i.Entries)
if numEntries != 2 { if numEntries != 2 {
t.Errorf("Expected 2 entries in index file but got %d", numEntries) 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) { func TestIndexDirectory(t *testing.T) {
dir := "testdata/repository" dir := "testdata/repository"
index, err := IndexDirectory(dir, "http://localhost:8080") index, err := NewChartRepositoryIndexFromDirectory(dir, "http://localhost:8080")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -305,8 +314,7 @@ func TestLoadUnversionedIndex(t *testing.T) {
} }
func TestIndexAdd(t *testing.T) { func TestIndexAdd(t *testing.T) {
i := NewChartRepositoryIndex()
i := NewIndexFile()
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: "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" { 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]) 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)) t := htemplate.Must(htemplate.New("index.html").Parse(indexHTMLTemplate))
// load index // load index
lrp := filepath.Join(s.RepoPath, "index.yaml") lrp := filepath.Join(s.RepoPath, "index.yaml")
i, err := LoadIndexFile(lrp) i, err := NewChartRepositoryIndexFromFile(lrp)
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return 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 // Reindex adds an entry to the index file at the given path
func Reindex(ch *chart.Chart, path string) error { func Reindex(ch *chart.Chart, path string) error {
name := ch.Metadata.Name + "-" + ch.Metadata.Version name := ch.Metadata.Name + "-" + ch.Metadata.Version
y, err := LoadIndexFile(path) y, err := NewChartRepositoryIndexFromFile(path)
if err != nil { if err != nil {
return err return err
} }

@ -21,64 +21,44 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings"
"time" "time"
"github.com/ghodss/yaml" "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 // ErrRepoOutOfDate indicates that the repository file is out of date, but
// is fixable. // is fixable.
var ErrRepoOutOfDate = errors.New("repository file is out of date") var ErrRepoOutOfDate = errors.New("repository file is out of date")
// ChartRepository represents a chart repository // RepositoryFile represents the repositories.yaml file in $HELM_HOME
type ChartRepository struct { type RepositoryFile struct {
RootPath string APIVersion string `json:"apiVersion"`
URL string // URL of repository Generated time.Time `json:"generated"`
ChartPaths []string Repositories []*ChartRepositoryConfig `json:"repositories"`
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"`
} }
// NewRepoFile generates an empty repositories file. // NewRepositoryFile generates an empty repositories file.
// //
// Generated and APIVersion are automatically set. // Generated and APIVersion are automatically set.
func NewRepoFile() *RepoFile { func NewRepositoryFile() *RepositoryFile {
return &RepoFile{ return &RepositoryFile{
APIVersion: APIVersionV1, APIVersion: APIVersionV1,
Generated: time.Now(), 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. // 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) b, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := &RepoFile{} r := &RepositoryFile{}
err = yaml.Unmarshal(b, r) err = yaml.Unmarshal(b, r)
if err != nil { if err != nil {
return nil, err return nil, err
@ -90,9 +70,9 @@ func LoadRepositoriesFile(path string) (*RepoFile, error) {
if err = yaml.Unmarshal(b, &m); err != nil { if err = yaml.Unmarshal(b, &m); err != nil {
return nil, err return nil, err
} }
r := NewRepoFile() r := NewRepositoryFile()
for k, v := range m { for k, v := range m {
r.Add(&Entry{ r.Add(&ChartRepositoryConfig{
Name: k, Name: k,
URL: v, URL: v,
Cache: fmt.Sprintf("%s-index.yaml", k), 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. // 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...) r.Repositories = append(r.Repositories, re...)
} }
// Update attempts to replace one or more repo entries in a repo file. If an // 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. // 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 { for _, target := range re {
found := false found := false
for j, repo := range r.Repositories { 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. // 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 { for _, rf := range r.Repositories {
if rf.Name == name { if rf.Name == name {
return true return true
@ -138,8 +118,8 @@ func (r *RepoFile) Has(name string) bool {
} }
// Remove removes the entry from the list of repositories. // Remove removes the entry from the list of repositories.
func (r *RepoFile) Remove(name string) bool { func (r *RepositoryFile) Remove(name string) bool {
cp := []*Entry{} cp := []*ChartRepositoryConfig{}
found := false found := false
for _, rf := range r.Repositories { for _, rf := range r.Repositories {
if rf.Name == name { if rf.Name == name {
@ -153,90 +133,10 @@ func (r *RepoFile) Remove(name string) bool {
} }
// WriteFile writes a repositories file to the given path. // 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) data, err := yaml.Marshal(r)
if err != nil { if err != nil {
return err return err
} }
return ioutil.WriteFile(path, data, perm) 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 package repo
import ( import "testing"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const testRepositoriesFile = "testdata/repositories.yaml" const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository"
const testURL = "http://example-charts.com"
func TestRepoFile(t *testing.T) { func TestRepoFile(t *testing.T) {
rf := NewRepoFile() rf := NewRepositoryFile()
rf.Add( rf.Add(
&Entry{ &ChartRepositoryConfig{
Name: "stable", Name: "stable",
URL: "https://example.com/stable/charts", URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml", Cache: "stable-index.yaml",
}, },
&Entry{ &ChartRepositoryConfig{
Name: "incubator", Name: "incubator",
URL: "https://example.com/incubator", URL: "https://example.com/incubator",
Cache: "incubator-index.yaml", Cache: "incubator-index.yaml",
@ -68,15 +58,15 @@ func TestRepoFile(t *testing.T) {
} }
} }
func TestLoadRepositoriesFile(t *testing.T) { func TestNewRepositoriesFile(t *testing.T) {
expects := NewRepoFile() expects := NewRepositoryFile()
expects.Add( expects.Add(
&Entry{ &ChartRepositoryConfig{
Name: "stable", Name: "stable",
URL: "https://example.com/stable/charts", URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml", Cache: "stable-index.yaml",
}, },
&Entry{ &ChartRepositoryConfig{
Name: "incubator", Name: "incubator",
URL: "https://example.com/incubator", URL: "https://example.com/incubator",
Cache: "incubator-index.yaml", 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") r, err := LoadRepositoriesFile("testdata/old-repositories.yaml")
if err != nil && err != ErrRepoOutOfDate { if err != nil && err != ErrRepoOutOfDate {
t.Fatal(err) t.Fatal(err)
@ -126,139 +116,3 @@ func TestLoadPreV1RepositoriesFile(t *testing.T) {
t.Errorf("expected the best charts ever. Got %#v", r.Repositories) 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" "github.com/ghodss/yaml"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
@ -69,7 +70,7 @@ func NewServer(docroot string) *Server {
} }
srv.start() srv.start()
// Add the testing repository as the only repo. // 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) panic(err)
} }
return srv return srv
@ -113,7 +114,7 @@ func (s *Server) CopyCharts(origin string) ([]string, error) {
// CreateIndex will read docroot and generate an index.yaml file. // CreateIndex will read docroot and generate an index.yaml file.
func (s *Server) CreateIndex() error { func (s *Server) CreateIndex() error {
// generate the index // generate the index
index, err := repo.IndexDirectory(s.docroot, s.URL()) index, err := repo.NewChartRepositoryIndexFromDirectory(s.docroot, s.URL())
if err != nil { if err != nil {
return err return err
} }
@ -158,11 +159,13 @@ func (s *Server) LinkIndices() error {
} }
// setTestingRepository sets up a testing repository.yaml with only the given name/URL. // setTestingRepository sets up a testing repository.yaml with only the given name/URL.
func setTestingRepository(helmhome, name, url string) error { func setTestingRepository(home helmpath.Home, name, url string) error {
rf := repo.NewRepoFile() r := repo.NewRepositoryFile()
rf.Add(&repo.Entry{Name: name, URL: url}) r.Add(&repo.ChartRepositoryConfig{
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) Name: name,
dest := filepath.Join(helmhome, "repository/repositories.yaml") URL: url,
Cache: home.CacheIndex(name),
return rf.WriteFile(dest, 0644) })
os.MkdirAll(filepath.Join(home.Repository(), name), 0755)
return r.WriteFile(home.RepositoryFile(), 0644)
} }

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