feat(helm): implement new index format

This implements a new index file format for repository indices. It also
implements a new format for requirements.yaml.

Breaking change: This will break all previous versions of Helm, and will
impact helm search, repo, serve, and fetch functions.

Closes #1197
pull/1257/head
Matt Butcher 8 years ago
parent c10e82e5d2
commit 4f09b05613

@ -62,7 +62,7 @@ test: test-unit
.PHONY: test-unit .PHONY: test-unit
test-unit: test-unit:
$(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) HELM_HOME=/no/such/dir $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS)
.PHONY: test-style .PHONY: test-style
test-style: test-style:

@ -30,7 +30,7 @@ import (
func TestDependencyBuildCmd(t *testing.T) { func TestDependencyBuildCmd(t *testing.T) {
oldhome := helmHome oldhome := helmHome
hh, err := tempHelmHome() hh, err := tempHelmHome(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -108,8 +108,12 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { reqver := i.Entries["reqtest"][0]
if h := reqver.Digest; h != hash {
t.Errorf("Failed hash match: expected %s, got %s", hash, h) t.Errorf("Failed hash match: expected %s, got %s", hash, h)
} }
if v := reqver.Version; v != "0.1.0" {
t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v)
}
} }

@ -36,7 +36,7 @@ import (
func TestDependencyUpdateCmd(t *testing.T) { func TestDependencyUpdateCmd(t *testing.T) {
// Set up a testing helm home // Set up a testing helm home
oldhome := helmHome oldhome := helmHome
hh, err := tempHelmHome() hh, err := tempHelmHome(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -90,7 +90,8 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if h := i.Entries["reqtest-0.1.0"].Digest; h != hash { reqver := i.Entries["reqtest"][0]
if h := reqver.Digest; h != hash {
t.Errorf("Failed hash match: expected %s, got %s", hash, h) t.Errorf("Failed hash match: expected %s, got %s", hash, h)
} }

@ -69,7 +69,7 @@ type ChartDownloader struct {
// For VerifyNever and VerifyIfPossible, the Verification may be empty. // For VerifyNever and VerifyIfPossible, the Verification may be empty.
func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) { func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) {
// resolve URL // resolve URL
u, err := c.ResolveChartRef(ref) u, err := c.ResolveChartVersion(ref)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -111,10 +111,10 @@ func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verif
return ver, nil return ver, nil
} }
// ResolveChartRef resolves a chart reference to a URL. // ResolveChartVersion resolves a chart reference to a URL.
// //
// A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) { func (c *ChartDownloader) ResolveChartVersion(ref string) (*url.URL, error) {
// See if it's already a full URL. // See if it's already a full URL.
u, err := url.ParseRequestURI(ref) u, err := url.ParseRequestURI(ref)
if err == nil { if err == nil {
@ -122,7 +122,7 @@ func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) {
if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
return u, nil return u, nil
} }
return u, fmt.Errorf("Invalid chart url format: %s", ref) return u, fmt.Errorf("invalid chart url format: %s", ref)
} }
r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
@ -133,15 +133,29 @@ func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) {
// See if it's of the form: repo/path_to_chart // See if it's of the form: repo/path_to_chart
p := strings.Split(ref, "/") p := strings.Split(ref, "/")
if len(p) > 1 { if len(p) > 1 {
if baseURL, ok := r.Repositories[p[0]]; ok { rf, err := findRepoEntry(p[0], r.Repositories)
if !strings.HasSuffix(baseURL, "/") { if err != nil {
baseURL = baseURL + "/" return u, err
} }
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) if rf.URL == "" {
return u, fmt.Errorf("no URL found for repository %q", p[0])
}
baseURL := rf.URL
if !strings.HasSuffix(baseURL, "/") {
baseURL = baseURL + "/"
}
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/"))
}
return u, fmt.Errorf("invalid chart url format: %s", ref)
}
func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) {
for _, re := range repos {
if re.Name == name {
return re, nil
} }
return u, fmt.Errorf("No such repo: %s", p[0])
} }
return u, fmt.Errorf("Invalid chart url format: %s", ref) return nil, fmt.Errorf("no repo named %q", name)
} }
// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.

@ -47,7 +47,7 @@ func TestResolveChartRef(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
u, err := c.ResolveChartRef(tt.ref) u, err := c.ResolveChartVersion(tt.ref)
if err != nil { if err != nil {
if tt.fail { if tt.fail {
continue continue

@ -175,8 +175,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
for _, dep := range deps { for _, dep := range deps {
fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
target := fmt.Sprintf("%s-%s", dep.Name, dep.Version) churl, err := findChartURL(dep.Name, dep.Repository, repos)
churl, err := findChartURL(target, dep.Repository, repos)
if err != nil { if err != nil {
fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err) fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err)
continue continue
@ -207,7 +206,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, dd.Repository) { if urlsAreEqual(repo.URL, dd.Repository) {
found = true found = true
} }
} }
@ -236,25 +235,23 @@ func (m *Manager) UpdateRepositories() error {
return nil return nil
} }
func (m *Manager) parallelRepoUpdate(repos map[string]string) { func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) {
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 name, url := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(n, u string) { go func(n, u string) {
err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)) if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); 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)
updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository: %s", n, err)
fmt.Fprintln(out, updateErr)
} 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", n)
} }
wg.Done() wg.Done()
}(name, url) }(re.Name, re.URL)
} }
wg.Wait() wg.Wait()
fmt.Fprintln(out, "Update Complete. Happy Helming!") fmt.Fprintln(out, "Update Complete. Happy Helming!")
} }
// urlsAreEqual normalizes two URLs and then compares for equality. // urlsAreEqual normalizes two URLs and then compares for equality.
@ -280,7 +277,15 @@ func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository)
if urlsAreEqual(repourl, cr.URL) { if urlsAreEqual(repourl, cr.URL) {
for ename, entry := range cr.IndexFile.Entries { for ename, entry := range cr.IndexFile.Entries {
if ename == name { if ename == name {
return entry.URL, nil for _, verEntry := range entry {
if len(verEntry.URLs) == 0 {
// Not totally sure what to do here. Returning an
// error is the strictest option. Skipping it might
// be preferable.
return "", fmt.Errorf("chart %q has no download URL", name)
}
return verEntry.URLs[0], nil
}
} }
} }
} }
@ -302,8 +307,8 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
} }
// localName: chartRepo for _, re := range rf.Repositories {
for lname, url := range rf.Repositories { lname := re.Name
cacheindex := m.HelmHome.CacheIndex(lname) cacheindex := m.HelmHome.CacheIndex(lname)
index, err := repo.LoadIndexFile(cacheindex) index, err := repo.LoadIndexFile(cacheindex)
if err != nil { if err != nil {
@ -311,7 +316,7 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
} }
cr := &repo.ChartRepository{ cr := &repo.ChartRepository{
URL: url, URL: re.URL,
IndexFile: index, IndexFile: index,
} }
indices[lname] = cr indices[lname] = cr

@ -1,38 +1,46 @@
alpine-0.1.0: apiVersion: v1
name: alpine entries:
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz alpine:
created: 2016-09-06 21:58:44.211261566 +0000 UTC - name: alpine
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
chartfile: checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
name: alpine home: https://k8s.io/helm
home: https://k8s.io/helm sources:
sources: - https://github.com/kubernetes/helm
- https://github.com/kubernetes/helm version: 0.1.0
version: 0.1.0 description: Deploy a basic Alpine Linux pod
description: Deploy a basic Alpine Linux pod keywords: []
keywords: [] maintainers: []
maintainers: [] engine: ""
engine: "" icon: ""
icon: "" - name: alpine
mariadb-0.3.0: url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
name: mariadb checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz home: https://k8s.io/helm
created: 2016-09-06 21:58:44.211870222 +0000 UTC sources:
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - https://github.com/kubernetes/helm
chartfile: version: 0.2.0
name: mariadb description: Deploy a basic Alpine Linux pod
home: https://mariadb.org keywords: []
sources: maintainers: []
- https://github.com/bitnami/bitnami-docker-mariadb engine: ""
version: 0.3.0 icon: ""
description: Chart for MariaDB mariadb:
keywords: - name: mariadb
- mariadb url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
- mysql checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
- database home: https://mariadb.org
- sql sources:
maintainers: - https://github.com/bitnami/bitnami-docker-mariadb
- name: Bitnami version: 0.3.0
email: containers@bitnami.com description: Chart for MariaDB
engine: gotpl keywords:
icon: "" - mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
engine: gotpl
icon: ""

@ -1 +1,4 @@
testing: "http://example.com" apiVersion: v1
repositories:
- name: testing
url: "http://example.com"

@ -26,7 +26,7 @@ import (
) )
func TestFetchCmd(t *testing.T) { func TestFetchCmd(t *testing.T) {
hh, err := tempHelmHome() hh, err := tempHelmHome(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -22,17 +22,20 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"os"
"regexp" "regexp"
"testing" "testing"
"github.com/golang/protobuf/ptypes/timestamp" "github.com/golang/protobuf/ptypes/timestamp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services" rls "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/proto/hapi/version" "k8s.io/helm/pkg/proto/hapi/version"
"k8s.io/helm/pkg/repo"
) )
var mockHookTemplate = `apiVersion: v1 var mockHookTemplate = `apiVersion: v1
@ -211,11 +214,11 @@ type releaseCase struct {
resp *release.Release resp *release.Release
} }
// tmpHelmHome sets up a Helm Home in a temp dir. // tempHelmHome sets up a Helm Home in a temp dir.
// //
// This does not clean up the directory. You must do that yourself. // This does not clean up the directory. You must do that yourself.
// You must also set helmHome yourself. // You must also set helmHome yourself.
func tempHelmHome() (string, error) { func tempHelmHome(t *testing.T) (string, error) {
oldhome := helmHome oldhome := helmHome
dir, err := ioutil.TempDir("", "helm_home-") dir, err := ioutil.TempDir("", "helm_home-")
if err != nil { if err != nil {
@ -223,9 +226,57 @@ func tempHelmHome() (string, error) {
} }
helmHome = dir helmHome = dir
if err := ensureHome(); err != nil { if err := ensureTestHome(helmpath.Home(helmHome), t); err != nil {
return "n/", err return "n/", err
} }
helmHome = oldhome helmHome = oldhome
return dir, nil return dir, nil
} }
// ensureTestHome creates a home directory like ensureHome, but without remote references.
//
// t is used only for logging.
func ensureTestHome(home helmpath.Home, t *testing.T) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
if err := os.MkdirAll(p, 0755); err != nil {
return fmt.Errorf("Could not create %s: %s", p, err)
}
} else if !fi.IsDir() {
return fmt.Errorf("%s must be a directory", p)
}
}
repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil {
rf := repo.NewRepoFile()
if err := rf.WriteFile(repoFile, 0644); err != nil {
return err
}
} 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 {
t.Log("Updating repository file format...")
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
}
localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath)
if fi, err := os.Stat(localRepoIndexFile); err != nil {
i := repo.NewIndexFile()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err
}
//TODO: take this out and replace with helm update functionality
os.Symlink(localRepoIndexFile, cacheDirectory("local-index.yaml"))
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile)
}
t.Logf("$HELM_HOME has been configured at %s.\n", helmHome)
return nil
}

@ -56,6 +56,9 @@ func (h Home) CacheIndex(name string) string {
// LocalRepository returns the location to the local repo. // LocalRepository returns the location to the local repo.
// //
// The local repo is the one used by 'helm serve' // The local repo is the one used by 'helm serve'
func (h Home) LocalRepository() string { //
return filepath.Join(string(h), "repository/local") // If additional path elements are passed, they are appended to the returned path.
func (h Home) LocalRepository(paths ...string) string {
frag := append([]string{string(h), "repository/local"}, paths...)
return filepath.Join(frag...)
} }

@ -25,7 +25,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/cmd/helm/installer" "k8s.io/helm/cmd/helm/installer"
"k8s.io/helm/pkg/repo"
) )
const initDesc = ` const initDesc = `
@ -33,15 +35,18 @@ This command installs Tiller (the helm server side component) onto your
Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.helm/) Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.helm/)
` `
var ( const (
defaultRepository = "stable" stableRepository = "stable"
defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts" localRepository = "local"
stableRepositoryURL = "http://storage.googleapis.com/kubernetes-charts"
localRepositoryURL = "http://localhost:8879/charts"
) )
type initCmd struct { type initCmd struct {
image string image string
clientOnly bool clientOnly bool
out io.Writer out io.Writer
home helmpath.Home
} }
func newInitCmd(out io.Writer) *cobra.Command { func newInitCmd(out io.Writer) *cobra.Command {
@ -56,6 +61,7 @@ func newInitCmd(out io.Writer) *cobra.Command {
if len(args) != 0 { if len(args) != 0 {
return errors.New("This command does not accept arguments") return errors.New("This command does not accept arguments")
} }
i.home = helmpath.Home(homePath())
return i.run() return i.run()
}, },
} }
@ -66,7 +72,7 @@ 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 err := ensureHome(); err != nil { if err := ensureHome(i.home, i.out); err != nil {
return err return err
} }
@ -101,12 +107,11 @@ func requireHome() error {
// ensureHome checks to see if $HELM_HOME exists // ensureHome 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() error { func ensureHome(home helmpath.Home, out io.Writer) error {
configDirectories := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()}
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.Printf("Creating %s \n", p) fmt.Fprintf(out, "Creating %s \n", p)
if err := os.MkdirAll(p, 0755); err != nil { if err := os.MkdirAll(p, 0755); err != nil {
return fmt.Errorf("Could not create %s: %s", p, err) return fmt.Errorf("Could not create %s: %s", p, err)
} }
@ -115,24 +120,41 @@ func ensureHome() error {
} }
} }
repoFile := repositoriesFile() repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil { if fi, err := os.Stat(repoFile); err != nil {
fmt.Printf("Creating %s \n", repoFile) fmt.Fprintf(out, "Creating %s \n", repoFile)
if _, err := os.Create(repoFile); err != nil { 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 {
return err return err
} }
if err := addRepository(defaultRepository, defaultRepositoryURL); err != nil { cif := home.CacheIndex(stableRepository)
return err if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil {
fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm update')\n", stableRepository, err)
} }
} 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 {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
}
localRepoIndexFile := localRepoDirectory(localRepoIndexFilePath) localRepoIndexFile := localRepoDirectory(localRepoIndexFilePath)
if fi, err := os.Stat(localRepoIndexFile); err != nil { if fi, err := os.Stat(localRepoIndexFile); err != nil {
fmt.Printf("Creating %s \n", localRepoIndexFile) fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile)
_, err := os.Create(localRepoIndexFile) i := repo.NewIndexFile()
if err != nil { if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err return err
} }
@ -142,6 +164,6 @@ func ensureHome() error {
return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile)
} }
fmt.Printf("$HELM_HOME has been configured at %s.\n", helmHome) fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", helmHome)
return nil return nil
} }

@ -17,28 +17,29 @@ limitations under the License.
package main package main
import ( import (
"fmt" "bytes"
"io/ioutil" "io/ioutil"
"net/http"
"net/http/httptest"
"os" "os"
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath"
) )
func TestEnsureHome(t *testing.T) { func TestEnsureHome(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { home, err := ioutil.TempDir("", "helm_home")
w.Header().Set("Content-Type", "text/plain") if err != nil {
fmt.Fprintln(w, "") t.Fatal(err)
})) }
defaultRepositoryURL = ts.URL defer os.Remove(home)
home := createTmpHome() b := bytes.NewBuffer(nil)
hh := helmpath.Home(home)
helmHome = home helmHome = home
if err := ensureHome(); err != nil { if err := ensureHome(hh, b); err != nil {
t.Errorf("%s", err) t.Error(err)
} }
expectedDirs := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} expectedDirs := []string{hh.String(), hh.Repository(), hh.Cache(), hh.LocalRepository()}
for _, dir := range expectedDirs { for _, dir := range expectedDirs {
if fi, err := os.Stat(dir); err != nil { if fi, err := os.Stat(dir); err != nil {
t.Errorf("%s", err) t.Errorf("%s", err)
@ -47,8 +48,8 @@ func TestEnsureHome(t *testing.T) {
} }
} }
if fi, err := os.Stat(repositoriesFile()); err != nil { if fi, err := os.Stat(hh.RepositoryFile()); err != nil {
t.Errorf("%s", err) t.Error(err)
} else if fi.IsDir() { } else if fi.IsDir() {
t.Errorf("%s should not be a directory", fi) t.Errorf("%s should not be a directory", fi)
} }
@ -59,9 +60,3 @@ func TestEnsureHome(t *testing.T) {
t.Errorf("%s should not be a directory", fi) t.Errorf("%s should not be a directory", fi)
} }
} }
func createTmpHome() string {
tmpHome, _ := ioutil.TempDir("", "helm_home")
defer os.Remove(tmpHome)
return tmpHome
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/provenance"
@ -50,6 +51,7 @@ type packageCmd struct {
key string key string
keyring string keyring string
out io.Writer out io.Writer
home helmpath.Home
} }
func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command {
@ -61,6 +63,7 @@ func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command {
Short: "package a chart directory into a chart archive", Short: "package a chart directory into a chart archive",
Long: packageDesc, Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
pkg.home = helmpath.Home(homePath())
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("This command needs at least one argument, the path to the chart.") return fmt.Errorf("This command needs at least one argument, the path to the chart.")
} }
@ -113,16 +116,17 @@ func (p *packageCmd) run(cmd *cobra.Command, args []string) error {
} }
name, err := chartutil.Save(ch, cwd) name, err := chartutil.Save(ch, cwd)
if err == nil && flagDebug { if err == nil && flagDebug {
cmd.Printf("Saved %s to current directory\n", name) fmt.Fprintf(p.out, "Saved %s to current directory\n", name)
} }
// Save to $HELM_HOME/local directory. This is second, because we don't want // Save to $HELM_HOME/local directory. This is second, because we don't want
// the case where we saved here, but didn't save to the default destination. // the case where we saved here, but didn't save to the default destination.
if p.save { if p.save {
if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { lr := p.home.LocalRepository()
if err := repo.AddChartToLocalRepo(ch, lr); err != nil {
return err return err
} else if flagDebug { } else if flagDebug {
cmd.Printf("Saved %s to %s\n", name, localRepoDirectory()) fmt.Fprintf(p.out, "Saved %s to %s\n", name, lr)
} }
} }

@ -24,6 +24,8 @@ import (
"testing" "testing"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
) )
func TestPackage(t *testing.T) { func TestPackage(t *testing.T) {
@ -57,6 +59,13 @@ func TestPackage(t *testing.T) {
expect: "keyring is required for signing a package", expect: "keyring is required for signing a package",
err: true, err: true,
}, },
{
name: "package testdata/testcharts/alpine, no save",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"save": "0"},
expect: "",
hasfile: "alpine-0.1.0.tgz",
},
{ {
name: "package testdata/testcharts/alpine", name: "package testdata/testcharts/alpine",
args: []string{"testdata/testcharts/alpine"}, args: []string{"testdata/testcharts/alpine"},
@ -87,7 +96,11 @@ func TestPackage(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
ensureTestHome(helmpath.Home(tmp), t)
oldhome := homePath()
helmHome = tmp
defer func() { defer func() {
helmHome = oldhome
os.Chdir(origDir) os.Chdir(origDir)
os.RemoveAll(tmp) os.RemoveAll(tmp)
}() }()

@ -17,20 +17,20 @@ limitations under the License.
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
type repoAddCmd struct { type repoAddCmd struct {
name string name string
url string url string
home helmpath.Home
out io.Writer out io.Writer
} }
@ -49,6 +49,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
add.name = args[0] add.name = args[0]
add.url = args[1] add.url = args[1]
add.home = helmpath.Home(homePath())
return add.run() return add.run()
}, },
@ -57,38 +58,36 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
} }
func (a *repoAddCmd) run() error { func (a *repoAddCmd) run() error {
if err := addRepository(a.name, a.url); err != nil { if err := addRepository(a.name, a.url, a.home); err != nil {
return err return err
} }
fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name)
fmt.Println(a.name + " has been added to your repositories")
return nil return nil
} }
func addRepository(name, url string) error { func addRepository(name, url string, home helmpath.Home) error {
if err := repo.DownloadIndexFile(name, url, cacheIndexFile(name)); err != nil { cif := home.CacheIndex(name)
return errors.New("Looks like " + url + " is not a valid chart repository or cannot be reached: " + err.Error()) 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) return insertRepoLine(name, url, home)
} }
func insertRepoLine(name, url string) error { func insertRepoLine(name, url string, home helmpath.Home) error {
f, err := repo.LoadRepositoriesFile(repositoriesFile()) cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil { if err != nil {
return err return err
} }
_, ok := f.Repositories[name]
if ok {
return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name)
}
if f.Repositories == nil { if f.Has(name) {
f.Repositories = make(map[string]string) return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name)
} }
f.Add(&repo.Entry{
f.Repositories[name] = url Name: name,
URL: url,
b, _ := yaml.Marshal(&f.Repositories) Cache: filepath.Base(cif),
return ioutil.WriteFile(repositoriesFile(), b, 0666) })
return f.WriteFile(home.RepositoryFile(), 0644)
} }

@ -18,29 +18,35 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath"
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
) )
var testName = "test-name" var testName = "test-name"
func TestRepoAddCmd(t *testing.T) { func TestRepoAddCmd(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := repotest.NewServer("testdata/testserver")
w.Header().Set("Content-Type", "text/plain") defer srv.Stop()
fmt.Fprintln(w, "")
})) thome, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}
oldhome := homePath()
helmHome = thome
defer func() {
helmHome = oldhome
os.Remove(thome)
}()
tests := []releaseCase{ tests := []releaseCase{
{ {
name: "add a repository", name: "add a repository",
args: []string{testName, ts.URL}, args: []string{testName, srv.URL()},
expected: testName + " has been added to your repositories", expected: testName + " has been added to your repositories",
}, },
} }
@ -49,41 +55,32 @@ func TestRepoAddCmd(t *testing.T) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
c := newRepoAddCmd(buf) c := newRepoAddCmd(buf)
if err := c.RunE(c, tt.args); err != nil { if err := c.RunE(c, tt.args); err != nil {
t.Errorf("%q: expected '%q', got '%q'", tt.name, tt.expected, err) t.Errorf("%q: expected %q, got %q", tt.name, tt.expected, err)
} }
} }
} }
func TestRepoAdd(t *testing.T) { func TestRepoAdd(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := repotest.NewServer("testdata/testserver")
w.Header().Set("Content-Type", "text/plain") defer ts.Stop()
fmt.Fprintln(w, "")
})) thome, err := tempHelmHome(t)
if err != nil {
helmHome, _ = ioutil.TempDir("", "helm_home") t.Fatal(err)
defer os.Remove(helmHome)
os.Mkdir(filepath.Join(helmHome, repositoryDir), 0755)
os.Mkdir(cacheDirectory(), 0755)
if err := ioutil.WriteFile(repositoriesFile(), []byte("example-repo: http://exampleurl.com"), 0666); err != nil {
t.Errorf("%#v", err)
} }
defer os.Remove(thome)
hh := helmpath.Home(thome)
if err := addRepository(testName, ts.URL); err != nil { if err := addRepository(testName, ts.URL(), hh); err != nil {
t.Errorf("%s", err) t.Error(err)
} }
f, err := repo.LoadRepositoriesFile(repositoriesFile()) f, err := repo.LoadRepositoriesFile(hh.RepositoryFile())
if err != nil { if err != nil {
t.Errorf("%s", err) t.Error(err)
}
_, ok := f.Repositories[testName]
if !ok {
t.Errorf("%s was not successfully inserted into %s", testName, repositoriesFile())
} }
if err := insertRepoLine(testName, ts.URL); err == nil { if !f.Has(testName) {
t.Errorf("Duplicate repository name was added") t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile())
} }
} }

@ -24,11 +24,13 @@ import (
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
type repoListCmd struct { type repoListCmd struct {
out io.Writer out io.Writer
home helmpath.Home
} }
func newRepoListCmd(out io.Writer) *cobra.Command { func newRepoListCmd(out io.Writer) *cobra.Command {
@ -40,6 +42,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command {
Use: "list [flags]", Use: "list [flags]",
Short: "list chart repositories", Short: "list chart repositories",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
list.home = helmpath.Home(homePath())
return list.run() return list.run()
}, },
} }
@ -48,7 +51,7 @@ func newRepoListCmd(out io.Writer) *cobra.Command {
} }
func (a *repoListCmd) run() error { func (a *repoListCmd) run() error {
f, err := repo.LoadRepositoriesFile(repositoriesFile()) f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile())
if err != nil { if err != nil {
return err return err
} }
@ -58,9 +61,9 @@ func (a *repoListCmd) run() error {
table := uitable.New() table := uitable.New()
table.MaxColWidth = 50 table.MaxColWidth = 50
table.AddRow("NAME", "URL") table.AddRow("NAME", "URL")
for k, v := range f.Repositories { for _, re := range f.Repositories {
table.AddRow(k, v) table.AddRow(re.Name, re.URL)
} }
fmt.Println(table) fmt.Fprintln(a.out, table)
return nil return nil
} }

@ -19,18 +19,18 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
type repoRemoveCmd struct { type repoRemoveCmd struct {
out io.Writer out io.Writer
name string name string
home helmpath.Home
} }
func newRepoRemoveCmd(out io.Writer) *cobra.Command { func newRepoRemoveCmd(out io.Writer) *cobra.Command {
@ -47,6 +47,7 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command {
return err return err
} }
remove.name = args[0] remove.name = args[0]
remove.home = helmpath.Home(homePath())
return remove.run() return remove.run()
}, },
@ -56,41 +57,35 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command {
} }
func (r *repoRemoveCmd) run() error { func (r *repoRemoveCmd) run() error {
return removeRepoLine(r.name) return removeRepoLine(r.out, r.name, r.home)
} }
func removeRepoLine(name string) error { func removeRepoLine(out io.Writer, name string, home helmpath.Home) error {
r, err := repo.LoadRepositoriesFile(repositoriesFile()) repoFile := home.RepositoryFile()
r, err := repo.LoadRepositoriesFile(repoFile)
if err != nil { if err != nil {
return err return err
} }
_, ok := r.Repositories[name] if !r.Remove(name) {
if ok { return fmt.Errorf("no repo named %q found", name)
delete(r.Repositories, name) }
b, err := yaml.Marshal(&r.Repositories) if err := r.WriteFile(repoFile, 0644); err != nil {
if err != nil { return err
return err }
}
if err := ioutil.WriteFile(repositoriesFile(), b, 0666); err != nil {
return err
}
if err := removeRepoCache(name); err != nil {
return err
}
} else { if err := removeRepoCache(name, home); err != nil {
return fmt.Errorf("The repository, %s, does not exist in your repositories list", name) return err
} }
fmt.Println(name + " has been removed from your repositories") fmt.Fprintf(out, "%q has been removed from your repositories", name)
return nil return nil
} }
func removeRepoCache(name string) error { func removeRepoCache(name string, home helmpath.Home) error {
if _, err := os.Stat(cacheIndexFile(name)); err == nil { if _, err := os.Stat(home.CacheIndex(name)); err == nil {
err = os.Remove(cacheIndexFile(name)) err = os.Remove(home.CacheIndex(name))
if err != nil { if err != nil {
return err return err
} }

@ -17,45 +17,52 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"os" "os"
"strings"
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
func TestRepoRemove(t *testing.T) { func TestRepoRemove(t *testing.T) {
testURL := "https://test-url.com" testURL := "https://test-url.com"
home := createTmpHome()
helmHome = home
if err := ensureHome(); err != nil {
t.Errorf("%s", err)
}
if err := removeRepoLine(testName); err == nil { b := bytes.NewBuffer(nil)
home, err := tempHelmHome(t)
defer os.Remove(home)
hh := helmpath.Home(home)
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 := insertRepoLine(testName, testURL); err != nil { t.Error(err)
t.Errorf("%s", err)
} }
mf, _ := os.Create(cacheIndexFile(testName)) mf, _ := os.Create(hh.CacheIndex(testName))
mf.Close() mf.Close()
if err := removeRepoLine(testName); err != nil { b.Reset()
if err := removeRepoLine(b, testName, hh); err != nil {
t.Errorf("Error removing %s from repositories", testName) t.Errorf("Error removing %s from repositories", testName)
} }
if !strings.Contains(b.String(), "has been removed") {
t.Errorf("Unexpected output: %s", b.String())
}
if _, err := os.Stat(cacheIndexFile(testName)); err == nil { if _, err := os.Stat(hh.CacheIndex(testName)); err == nil {
t.Errorf("Error cache file was not removed for repository %s", testName) t.Errorf("Error cache file was not removed for repository %s", testName)
} }
f, err := repo.LoadRepositoriesFile(repositoriesFile()) f, err := repo.LoadRepositoriesFile(hh.RepositoryFile())
if err != nil { if err != nil {
t.Errorf("%s", err) t.Error(err)
} }
if _, ok := f.Repositories[testName]; ok { if f.Has(testName) {
t.Errorf("%s was not successfully removed from repositories list", testName) t.Errorf("%s was not successfully removed from repositories list", testName)
} }
} }

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
@ -37,8 +38,9 @@ future releases.
type repoUpdateCmd struct { type repoUpdateCmd struct {
repoFile string repoFile string
update func(map[string]string, bool, io.Writer) update func([]*repo.Entry, bool, io.Writer, helmpath.Home)
out io.Writer out io.Writer
home helmpath.Home
} }
func newRepoUpdateCmd(out io.Writer) *cobra.Command { func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -53,6 +55,7 @@ func newRepoUpdateCmd(out io.Writer) *cobra.Command {
Short: "update information on available charts in the chart repositories", Short: "update information on available charts in the chart repositories",
Long: updateDesc, Long: updateDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
u.home = helmpath.Home(homePath())
return u.run() return u.run()
}, },
} }
@ -69,29 +72,29 @@ func (u *repoUpdateCmd) run() error {
return errors.New("no repositories found. You must add one before updating") return errors.New("no repositories found. You must add one before updating")
} }
u.update(f.Repositories, flagDebug, u.out) u.update(f.Repositories, flagDebug, u.out, u.home)
return nil return nil
} }
func updateCharts(repos map[string]string, verbose bool, out io.Writer) { func updateCharts(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) {
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 name, url := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(n, u string) { go func(n, u string) {
defer wg.Done() defer wg.Done()
err := repo.DownloadIndexFile(n, u, cacheIndexFile(n)) if n == localRepository {
// We skip local because the indices are symlinked.
return
}
err := repo.DownloadIndexFile(n, u, home.CacheIndex(n))
if err != nil { if err != nil {
updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository", n) fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
if verbose {
updateErr = updateErr + ": " + err.Error()
}
fmt.Fprintln(out, updateErr)
} 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", n)
} }
}(name, url) }(re.Name, re.URL)
} }
wg.Wait() wg.Wait()
fmt.Fprintln(out, "Update Complete. Happy Helming!") fmt.Fprintln(out, "Update Complete. Happy Helming!")
} }

@ -19,19 +19,34 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"net/http" "os"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
) )
func TestUpdateCmd(t *testing.T) { func TestUpdateCmd(t *testing.T) {
thome, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}
oldhome := homePath()
helmHome = thome
defer func() {
helmHome = oldhome
os.Remove(thome)
}()
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 map[string]string, verbose bool, out io.Writer) { updater := func(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) {
for name := range repos { for _, re := range repos {
fmt.Fprintln(out, name) fmt.Fprintln(out, re.Name)
} }
} }
uc := &repoUpdateCmd{ uc := &repoUpdateCmd{
@ -46,36 +61,26 @@ func TestUpdateCmd(t *testing.T) {
} }
} }
const mockRepoIndex = `
mychart-0.1.0:
name: mychart-0.1.0
url: localhost:8879/charts/mychart-0.1.0.tgz
chartfile:
name: ""
home: ""
sources: []
version: ""
description: ""
keywords: []
maintainers: []
engine: ""
icon: ""
`
func TestUpdateCharts(t *testing.T) { func TestUpdateCharts(t *testing.T) {
// This tests the repo in isolation. It creates a mock HTTP server that simply srv := repotest.NewServer("testdata/testserver")
// returns a static YAML file in the anticipate format. defer srv.Stop()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(mockRepoIndex)) thome, err := tempHelmHome(t)
}) if err != nil {
srv := httptest.NewServer(handler) t.Fatal(err)
defer srv.Close() }
oldhome := homePath()
helmHome = thome
defer func() {
helmHome = oldhome
os.Remove(thome)
}()
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
repos := map[string]string{ repos := []*repo.Entry{
"charts": srv.URL, {Name: "charts", URL: srv.URL()},
} }
updateCharts(repos, false, buf) updateCharts(repos, false, buf, helmpath.Home(thome))
got := buf.String() got := buf.String()
if strings.Contains(got, "Unable to get an update") { if strings.Contains(got, "Unable to get an update") {

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

@ -44,27 +44,34 @@ type Result struct {
// Index is a searchable index of chart information. // Index is a searchable index of chart information.
type Index struct { type Index struct {
lines map[string]string lines map[string]string
charts map[string]*repo.ChartRef charts map[string]*repo.ChartVersion
} }
const sep = "\v" const sep = "\v"
// NewIndex creats a new Index. // NewIndex creats a new Index.
func NewIndex() *Index { func NewIndex() *Index {
return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartRef{}} return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}}
} }
// 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) { func (i *Index) AddRepo(rname string, ind *repo.IndexFile) {
for name, ref := range ind.Entries { for name, ref := range ind.Entries {
if len(ref) == 0 {
// Skip chart names that havae zero releases.
continue
}
// By convention, an index file is supposed to have the newest at the
// 0 slot, so our best bet is to grab the 0 entry and build the index
// entry off of that.
fname := filepath.Join(rname, name) fname := filepath.Join(rname, name)
i.lines[fname] = indstr(rname, ref) i.lines[fname] = indstr(rname, ref[0])
i.charts[fname] = ref i.charts[fname] = ref[0]
} }
} }
// Entries returns the entries in an index. // Entries returns the entries in an index.
func (i *Index) Entries() map[string]*repo.ChartRef { func (i *Index) Entries() map[string]*repo.ChartVersion {
return i.charts return i.charts
} }
@ -136,7 +143,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
} }
// Chart returns the ChartRef for a particular name. // Chart returns the ChartRef for a particular name.
func (i *Index) Chart(name string) (*repo.ChartRef, error) { func (i *Index) Chart(name string) (*repo.ChartVersion, error) {
c, ok := i.charts[name] c, ok := i.charts[name]
if !ok { if !ok {
return nil, errors.New("no such chart") return nil, errors.New("no such chart")
@ -174,10 +181,8 @@ func (s scoreSorter) Less(a, b int) bool {
return first.Name < second.Name return first.Name < second.Name
} }
func indstr(name string, ref *repo.ChartRef) string { func indstr(name string, ref *repo.ChartVersion) string {
i := ref.Name + sep + name + "/" + ref.Name + sep i := ref.Name + sep + name + "/" + ref.Name + sep +
if ref.Chartfile != nil { ref.Description + sep + strings.Join(ref.Keywords, " ")
i += ref.Chartfile.Description + sep + strings.Join(ref.Chartfile.Keywords, sep)
}
return strings.ToLower(i) return strings.ToLower(i)
} }

@ -52,32 +52,43 @@ func TestSortScore(t *testing.T) {
var testCacheDir = "../testdata/" var testCacheDir = "../testdata/"
var indexfileEntries = map[string]*repo.ChartRef{ var indexfileEntries = map[string]repo.ChartVersions{
"niña-0.1.0": { "niña": {
Name: "niña", {
URL: "http://example.com/charts/nina-0.1.0.tgz", URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"},
Chartfile: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "niña", Name: "niña",
Version: "0.1.0", Version: "0.1.0",
Description: "One boat", Description: "One boat",
},
}, },
}, },
"pinta-0.1.0": { "pinta": {
Name: "pinta", {
URL: "http://example.com/charts/pinta-0.1.0.tgz", URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"},
Chartfile: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "pinta", Name: "pinta",
Version: "0.1.0", Version: "0.1.0",
Description: "Two ship", Description: "Two ship",
},
}, },
}, },
"santa-maria-1.2.3": { "santa-maria": {
Name: "santa-maria", {
URL: "http://example.com/charts/santa-maria-1.2.3.tgz", URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"},
Chartfile: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "santa-maria", Name: "santa-maria",
Version: "1.2.3", Version: "1.2.3",
Description: "Three boat", Description: "Three boat",
},
},
{
URLs: []string{"http://example.com/charts/santa-maria-1.2.2.tgz"},
Metadata: &chart.Metadata{
Name: "santa-maria",
Version: "1.2.2",
Description: "Three boat",
},
}, },
}, },
} }
@ -85,14 +96,15 @@ var indexfileEntries = map[string]*repo.ChartRef{
func loadTestIndex(t *testing.T) *Index { func loadTestIndex(t *testing.T) *Index {
i := NewIndex() i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}) i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries})
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]*repo.ChartRef{ i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta-2.0.0": { "pinta": {
Name: "pinta", {
URL: "http://example.com/charts/pinta-2.0.0.tgz", URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"},
Chartfile: &chart.Metadata{ Metadata: &chart.Metadata{
Name: "pinta", Name: "pinta",
Version: "2.0.0", Version: "2.0.0",
Description: "Two ship, version two", Description: "Two ship, version two",
},
}, },
}, },
}}) }})
@ -113,44 +125,44 @@ func TestSearchByName(t *testing.T) {
name: "basic search for one result", name: "basic search for one result",
query: "santa-maria", query: "santa-maria",
expect: []*Result{ expect: []*Result{
{Name: "testing/santa-maria-1.2.3"}, {Name: "testing/santa-maria"},
}, },
}, },
{ {
name: "basic search for two results", name: "basic search for two results",
query: "pinta", query: "pinta",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta-0.1.0"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta-2.0.0"}, {Name: "ztesting/pinta"},
}, },
}, },
{ {
name: "repo-specific search for one result", name: "repo-specific search for one result",
query: "ztesting/pinta", query: "ztesting/pinta",
expect: []*Result{ expect: []*Result{
{Name: "ztesting/pinta-2.0.0"}, {Name: "ztesting/pinta"},
}, },
}, },
{ {
name: "partial name search", name: "partial name search",
query: "santa", query: "santa",
expect: []*Result{ expect: []*Result{
{Name: "testing/santa-maria-1.2.3"}, {Name: "testing/santa-maria"},
}, },
}, },
{ {
name: "description search, one result", name: "description search, one result",
query: "Three", query: "Three",
expect: []*Result{ expect: []*Result{
{Name: "testing/santa-maria-1.2.3"}, {Name: "testing/santa-maria"},
}, },
}, },
{ {
name: "description search, two results", name: "description search, two results",
query: "two", query: "two",
expect: []*Result{ expect: []*Result{
{Name: "testing/pinta-0.1.0"}, {Name: "testing/pinta"},
{Name: "ztesting/pinta-2.0.0"}, {Name: "ztesting/pinta"},
}, },
}, },
{ {
@ -162,7 +174,7 @@ func TestSearchByName(t *testing.T) {
name: "regexp, one result", name: "regexp, one result",
query: "th[ref]*", query: "th[ref]*",
expect: []*Result{ expect: []*Result{
{Name: "testing/santa-maria-1.2.3"}, {Name: "testing/santa-maria"},
}, },
regexp: true, regexp: true,
}, },

@ -34,12 +34,12 @@ func TestSearchCmd(t *testing.T) {
{ {
name: "search for 'maria', expect one match", name: "search for 'maria', expect one match",
args: []string{"maria"}, args: []string{"maria"},
expect: "testing/mariadb-0.3.0", expect: "testing/mariadb",
}, },
{ {
name: "search for 'alpine', expect two matches", name: "search for 'alpine', expect two matches",
args: []string{"alpine"}, args: []string{"alpine"},
expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", expect: "testing/alpine",
}, },
{ {
name: "search for 'syzygy', expect no matches", name: "search for 'syzygy', expect no matches",
@ -50,7 +50,7 @@ func TestSearchCmd(t *testing.T) {
name: "search for 'alp[a-z]+', expect two matches", name: "search for 'alp[a-z]+', expect two matches",
args: []string{"alp[a-z]+"}, args: []string{"alp[a-z]+"},
flags: []string{"--regexp"}, flags: []string{"--regexp"},
expect: "testing/alpine-0.1.0\ntesting/alpine-0.2.0", expect: "testing/alpine",
regexp: true, regexp: true,
}, },
{ {

@ -17,35 +17,40 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
const serveDesc = `This command starts a local chart repository server that serves charts from a local directory.` const serveDesc = `This command starts a local chart repository server that serves charts from a local directory.`
type serveCmd struct { type serveCmd struct {
repoPath string
out io.Writer out io.Writer
home helmpath.Home
address string
repoPath string
} }
func newServeCmd(out io.Writer) *cobra.Command { func newServeCmd(out io.Writer) *cobra.Command {
s := &serveCmd{ srv := &serveCmd{out: out}
out: out,
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "serve", Use: "serve",
Short: "start a local http web server", Short: "start a local http web server",
Long: serveDesc, Long: serveDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return s.run() srv.home = helmpath.Home(homePath())
return srv.run()
}, },
} }
cmd.Flags().StringVar(&s.repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.") cmd.Flags().StringVar(&srv.repoPath, "repo-path", helmpath.Home(homePath()).LocalRepository(), "The local directory path from which to serve charts.")
cmd.Flags().StringVar(&srv.address, "address", "localhost:8879", "The address to listen on.")
return cmd return cmd
} }
@ -58,6 +63,6 @@ func (s *serveCmd) run() error {
return err return err
} }
repo.StartLocalRepo(s.repoPath) fmt.Fprintf(s.out, "Now serving you on %s\n", s.address)
return nil return repo.StartLocalRepo(repoPath, s.address)
} }

@ -1,54 +1,46 @@
alpine-0.1.0: apiVersion: v1
name: alpine entries:
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz alpine:
created: 2016-09-06 21:58:44.211261566 +0000 UTC - name: alpine
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
chartfile: checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
name: alpine home: https://k8s.io/helm
home: https://k8s.io/helm sources:
sources: - https://github.com/kubernetes/helm
- https://github.com/kubernetes/helm version: 0.1.0
version: 0.1.0 description: Deploy a basic Alpine Linux pod
description: Deploy a basic Alpine Linux pod keywords: []
keywords: [] maintainers: []
maintainers: [] engine: ""
engine: "" icon: ""
icon: "" - name: alpine
alpine-0.2.0: url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
name: alpine checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz home: https://k8s.io/helm
created: 2016-09-06 21:58:44.211261566 +0000 UTC sources:
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - https://github.com/kubernetes/helm
chartfile: version: 0.2.0
name: alpine description: Deploy a basic Alpine Linux pod
home: https://k8s.io/helm keywords: []
sources: maintainers: []
- https://github.com/kubernetes/helm engine: ""
version: 0.2.0 icon: ""
description: Deploy a basic Alpine Linux pod mariadb:
keywords: [] - name: mariadb
maintainers: [] url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
engine: "" checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
icon: "" home: https://mariadb.org
mariadb-0.3.0: sources:
name: mariadb - https://github.com/bitnami/bitnami-docker-mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz version: 0.3.0
created: 2016-09-06 21:58:44.211870222 +0000 UTC description: Chart for MariaDB
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 keywords:
chartfile: - mariadb
name: mariadb - mysql
home: https://mariadb.org - database
sources: - sql
- https://github.com/bitnami/bitnami-docker-mariadb maintainers:
version: 0.3.0 - name: Bitnami
description: Chart for MariaDB email: containers@bitnami.com
keywords: engine: gotpl
- mariadb icon: ""
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
engine: gotpl
icon: ""

@ -1 +1,6 @@
testing: http://example.com/charts apiVersion: v1
generated: 2016-10-03T16:03:10.640376913-06:00
repositories:
- cache: testing-index.yaml
name: testing
url: http://example.com/charts

@ -1,2 +1,6 @@
charts: http://storage.googleapis.com/kubernetes-charts apiVersion: v1
local: http://localhost:8879/charts repositories:
- name: charts
url: "http://storage.googleapis.com/kubernetes-charts"
- name: local
url: "http://localhost:8879/charts"

@ -0,0 +1 @@
apiVersion: v1

@ -0,0 +1,6 @@
apiVersion: v1
generated: 2016-10-04T13:50:02.87649685-06:00
repositories:
- cache: ""
name: test
url: http://127.0.0.1:49216

@ -19,26 +19,45 @@ The index file is a yaml file called `index.yaml`. It contains some metadata abo
This is an example of an index file: This is an example of an index file:
``` ```
alpine-0.1.0: apiVersion: v1
name: alpine entries:
url: https://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz nginx:
created: 2016-05-26 11:23:44.086354411 +0000 UTC - urls:
digest: sha256:78e9a4282295184e8ce1496d23987993673f38e33e203c8bc18bc838a73e5864 - http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz
chartfile: name: nginx
name: alpine description: string
description: Deploy a basic Alpine Linux pod version: 0.1.0
version: 0.1.0 home: https://github.com/something
home: https://github.com/example-charts/alpine digest: "sha256:1234567890abcdef"
redis-2.0.0: keywords:
name: redis - popular
url: https://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz - web server
created: 2016-05-26 11:23:44.087939192 +0000 UTC - proxy
digest: sha256:bde9c2949e64d059c18d8f93566a64dafc6d2e8e259a70322fb804831dfd0b5b - urls:
chartfile: - http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz
name: redis name: nginx
description: Port of the replicatedservice template from kubernetes/charts description: string
version: 2.0.0 version: 0.2.0
home: https://github.com/example-charts/redis home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
alpine:
- urls:
- http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine
description: string
version: 1.0.0
home: https://github.com/something
keywords:
- linux
- alpine
- small
- sumtin
digest: "sha256:1234567890abcdef"
``` ```
We will go through detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository. We will go through detailed GCS and Github Pages examples here, but feel free to skip to the next section if you've already created a chart repository.

@ -0,0 +1,93 @@
/*
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 implements the Helm Chart Repository.
A chart repository is an HTTP server that provides information on charts. A local
repository cache is an on-disk representation of a chart repository.
There are two important file formats for chart repositories.
The first is the 'index.yaml' format, which is expressed like this:
apiVersion: v1
entries:
frobnitz:
- created: 2016-09-29T12:14:34.830161306-06:00
description: This is a frobniz.
digest: 587bd19a9bd9d2bc4a6d25ab91c8c8e7042c47b4ac246e37bf8e1e74386190f4
home: http://example.com
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- email: helm@example.com
name: The Helm Team
- email: nobody@example.com
name: Someone Else
name: frobnitz
urls:
- http://example-charts.com/testdata/repository/frobnitz-1.2.3.tgz
version: 1.2.3
sprocket:
- created: 2016-09-29T12:14:34.830507606-06:00
description: This is a sprocket"
digest: 8505ff813c39502cc849a38e1e4a8ac24b8e6e1dcea88f4c34ad9b7439685ae6
home: http://example.com
keywords:
- frobnitz
- sprocket
- dodad
maintainers:
- email: helm@example.com
name: The Helm Team
- email: nobody@example.com
name: Someone Else
name: sprocket
urls:
- http://example-charts.com/testdata/repository/sprocket-1.2.0.tgz
version: 1.2.0
generated: 2016-09-29T12:14:34.829721375-06:00
An index.yaml file contains the necessary descriptive information about what
charts are available in a repository, and how to get them.
The second file format is the repositories.yaml file format. This file is for
facilitating local cached copies of one or more chart repositories.
The format of a repository.yaml file is:
apiVersion: v1
generated: TIMESTAMP
repositories:
- name: stable
url: http://example.com/charts
cache: stable-index.yaml
- name: incubator
url: http://example.com/incubator
cache: incubator-index.yaml
This file maps three bits of information about a repository:
- The name the user uses to refer to it
- The fully qualified URL to the repository (index.yaml will be appended)
- The name of the local cachefile
The format for both files was changed after Helm v2.0.0-Alpha.4. Helm is not
backwards compatible with those earlier versions.
*/
package repo

@ -17,12 +17,17 @@ limitations under the License.
package repo package repo
import ( import (
"errors"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"time"
"gopkg.in/yaml.v2" "github.com/Masterminds/semver"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
@ -31,39 +36,116 @@ import (
var indexPath = "index.yaml" var indexPath = "index.yaml"
// APIVersionV1 is the v1 API version for index and repository files.
const APIVersionV1 = "v1"
// ErrNoAPIVersion indicates that an API version was not specified.
var ErrNoAPIVersion = errors.New("no API version specified")
// ChartVersions is a list of versioned chart references.
// Implements a sorter on Version.
type ChartVersions []*ChartVersion
// Len returns the length.
func (c ChartVersions) Len() int { return len(c) }
// Swap swaps the position of two items in the versions slice.
func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// Less returns true if the version of entry a is less than the version of entry b.
func (c ChartVersions) Less(a, b int) bool {
// Failed parse pushes to the back.
i, err := semver.NewVersion(c[a].Version)
if err != nil {
return true
}
j, err := semver.NewVersion(c[b].Version)
if err != nil {
return false
}
return i.LessThan(j)
}
// IndexFile represents the index file in a chart repository // IndexFile represents the index file in a chart repository
type IndexFile struct { type IndexFile struct {
Entries map[string]*ChartRef APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"`
Entries map[string]ChartVersions `json:"entries"`
PublicKeys []string `json:"publicKeys,omitempty"`
} }
// NewIndexFile initializes an index. // NewIndexFile initializes an index.
func NewIndexFile() *IndexFile { func NewIndexFile() *IndexFile {
return &IndexFile{Entries: map[string]*ChartRef{}} return &IndexFile{
APIVersion: APIVersionV1,
Generated: time.Now(),
Entries: map[string]ChartVersions{},
PublicKeys: []string{},
}
} }
// Add adds a file to the index // Add adds a file to the index
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
name := strings.TrimSuffix(filename, ".tgz") cr := &ChartVersion{
cr := &ChartRef{ URLs: []string{baseURL + "/" + filename},
Name: name, Metadata: md,
URL: baseURL + "/" + filename, Digest: digest,
Chartfile: md, Created: time.Now(),
Digest: digest, }
Created: nowString(), if ee, ok := i.Entries[md.Name]; !ok {
} i.Entries[md.Name] = ChartVersions{cr}
i.Entries[name] = cr } else {
i.Entries[md.Name] = append(ee, cr)
}
}
// 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 {
vs, ok := i.Entries[name]
if !ok {
return false
}
for _, ver := range vs {
// TODO: Do we need to normalize the version field with the SemVer lib?
if ver.Version == version {
return true
}
}
return false
}
// SortEntries sorts the entries by version in descending order.
//
// In canonical form, the individual version records should be sorted so that
// 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() {
for _, versions := range i.Entries {
sort.Sort(sort.Reverse(versions))
}
}
// 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 {
b, err := yaml.Marshal(i)
if err != nil {
return err
}
return ioutil.WriteFile(dest, b, mode)
} }
// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 // Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2
// ChartRef represents a chart entry in the IndexFile // ChartVersion represents a chart entry in the IndexFile
type ChartRef struct { type ChartVersion struct {
Name string `yaml:"name" json:"name"` *chart.Metadata
URL string `yaml:"url" json:"url"` URLs []string `yaml:"url" json:"urls"`
Created string `yaml:"created,omitempty" json:"created,omitempty"` Created time.Time `yaml:"created,omitempty" json:"created,omitempty"`
Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"` Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"` Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
Chartfile *chart.Metadata `yaml:"chartfile" json:"chartfile"`
} }
// IndexDirectory reads a (flat) directory and generates an index. // IndexDirectory reads a (flat) directory and generates an index.
@ -104,42 +186,30 @@ func DownloadIndexFile(repoName, url, indexFilePath string) error {
} }
defer resp.Body.Close() defer resp.Body.Close()
var r IndexFile
b, err := ioutil.ReadAll(resp.Body) b, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }
if err := yaml.Unmarshal(b, &r); err != nil { if _, err := LoadIndex(b); err != nil {
return err return err
} }
return ioutil.WriteFile(indexFilePath, b, 0644) return ioutil.WriteFile(indexFilePath, b, 0644)
} }
// UnmarshalYAML unmarshals the index file // LoadIndex loads an index file and does minimal validity checking.
func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error { //
var refs map[string]*ChartRef // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
if err := unmarshal(&refs); err != nil { func LoadIndex(data []byte) (*IndexFile, error) {
return err i := &IndexFile{}
} if err := yaml.Unmarshal(data, i); err != nil {
i.Entries = refs return i, err
return nil
}
func (i *IndexFile) addEntry(name string, url string) ([]byte, error) {
if i.Entries == nil {
i.Entries = make(map[string]*ChartRef)
} }
entry := ChartRef{Name: name, URL: url} if i.APIVersion == "" {
i.Entries[name] = &entry return i, ErrNoAPIVersion
out, err := yaml.Marshal(&i.Entries)
if err != nil {
return nil, err
} }
return i, nil
return out, nil
} }
// LoadIndexFile takes a file at the given path and returns an IndexFile object // LoadIndexFile takes a file at the given path and returns an IndexFile object
@ -148,12 +218,5 @@ func LoadIndexFile(path string) (*IndexFile, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return LoadIndex(b)
indexfile := NewIndexFile()
err = yaml.Unmarshal(b, indexfile)
if err != nil {
return nil, err
}
return indexfile, nil
} }

@ -17,7 +17,6 @@ limitations under the License.
package repo package repo
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -25,25 +24,74 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gopkg.in/yaml.v2" "k8s.io/helm/pkg/proto/hapi/chart"
) )
const testfile = "testdata/local-index.yaml" const (
testfile = "testdata/local-index.yaml"
var (
testRepo = "test-repo" testRepo = "test-repo"
) )
func TestIndexFile(t *testing.T) {
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: "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.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc")
i.SortEntries()
if i.APIVersion != APIVersionV1 {
t.Error("Expected API version v1")
}
if len(i.Entries) != 2 {
t.Errorf("Expected 2 charts. Got %d", len(i.Entries))
}
if i.Entries["clipper"][0].Name != "clipper" {
t.Errorf("Expected clipper, got %s", i.Entries["clipper"][0].Name)
}
if len(i.Entries["cutter"]) != 3 {
t.Error("Expected two cutters.")
}
// Test that the sort worked. 0.2 should be at the first index for Cutter.
if v := i.Entries["cutter"][0].Version; v != "0.2.0" {
t.Errorf("Unexpected first version: %s", v)
}
}
func TestLoadIndex(t *testing.T) {
b, err := ioutil.ReadFile(testfile)
if err != nil {
t.Fatal(err)
}
i, err := LoadIndex(b)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, i)
}
func TestLoadIndexFile(t *testing.T) {
i, err := LoadIndexFile(testfile)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, i)
}
func TestDownloadIndexFile(t *testing.T) { func TestDownloadIndexFile(t *testing.T) {
fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml")
if err != nil { if err != nil {
t.Errorf("%#v", err) t.Fatal(err)
} }
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "binary/octet-stream") w.Write(fileBytes)
fmt.Fprintln(w, string(fileBytes))
})) }))
defer srv.Close()
dirName, err := ioutil.TempDir("", "tmp") dirName, err := ioutil.TempDir("", "tmp")
if err != nil { if err != nil {
@ -52,7 +100,7 @@ func TestDownloadIndexFile(t *testing.T) {
defer os.RemoveAll(dirName) defer os.RemoveAll(dirName)
path := filepath.Join(dirName, testRepo+"-index.yaml") path := filepath.Join(dirName, testRepo+"-index.yaml")
if err := DownloadIndexFile(testRepo, ts.URL, path); err != nil { if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil {
t.Errorf("%#v", err) t.Errorf("%#v", err)
} }
@ -65,50 +113,111 @@ func TestDownloadIndexFile(t *testing.T) {
t.Errorf("error reading index file: %#v", err) t.Errorf("error reading index file: %#v", err)
} }
var i IndexFile i, err := LoadIndex(b)
if err = yaml.Unmarshal(b, &i); err != nil { if err != nil {
t.Errorf("error unmarshaling index file: %#v", err) t.Errorf("Index %q failed to parse: %s", testfile, err)
return
} }
verifyLocalIndex(t, i)
}
func verifyLocalIndex(t *testing.T, i *IndexFile) {
numEntries := len(i.Entries) numEntries := len(i.Entries)
if numEntries != 2 { if numEntries != 2 {
t.Errorf("Expected 2 entries in index file but got %v", numEntries) t.Errorf("Expected 2 entries in index file but got %d", numEntries)
} }
os.Remove(path)
}
func TestLoadIndexFile(t *testing.T) { alpine, ok := i.Entries["alpine"]
cf, err := LoadIndexFile(testfile) if !ok {
if err != nil { t.Errorf("'alpine' section not found.")
t.Errorf("Failed to load index file: %s", err) return
} }
if len(cf.Entries) != 2 {
t.Errorf("Expected 2 entries in the index file, but got %d", len(cf.Entries)) if l := len(alpine); l != 1 {
} t.Errorf("'alpine' should have 1 chart, got %d", l)
nginx := false return
alpine := false }
for k, e := range cf.Entries {
if k == "nginx-0.1.0" { nginx, ok := i.Entries["nginx"]
if e.Name == "nginx" { if !ok || len(nginx) != 2 {
if len(e.Chartfile.Keywords) == 3 { t.Error("Expected 2 nginx entries")
nginx = true return
} }
expects := []*ChartVersion{
{
Metadata: &chart.Metadata{
Name: "alpine",
Description: "string",
Version: "1.0.0",
Keywords: []string{"linux", "alpine", "small", "sumtin"},
Home: "https://github.com/something",
},
URLs: []string{
"http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz",
"http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz",
},
Digest: "sha256:1234567890abcdef",
},
{
Metadata: &chart.Metadata{
Name: "nginx",
Description: "string",
Version: "0.1.0",
Keywords: []string{"popular", "web server", "proxy"},
Home: "https://github.com/something",
},
URLs: []string{
"http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz",
},
Digest: "sha256:1234567890abcdef",
},
{
Metadata: &chart.Metadata{
Name: "nginx",
Description: "string",
Version: "0.2.0",
Keywords: []string{"popular", "web server", "proxy"},
Home: "https://github.com/something/else",
},
URLs: []string{
"http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz",
},
Digest: "sha256:1234567890abcdef",
},
}
tests := []*ChartVersion{alpine[0], nginx[0], nginx[1]}
for i, tt := range tests {
expect := expects[i]
if tt.Name != expect.Name {
t.Errorf("Expected name %q, got %q", expect.Name, tt.Name)
}
if tt.Description != expect.Description {
t.Errorf("Expected description %q, got %q", expect.Description, tt.Description)
}
if tt.Version != expect.Version {
t.Errorf("Expected version %q, got %q", expect.Version, tt.Version)
}
if tt.Digest != expect.Digest {
t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest)
}
if tt.Home != expect.Home {
t.Errorf("Expected home %q, got %q", expect.Home, tt.Home)
}
for i, url := range tt.URLs {
if url != expect.URLs[i] {
t.Errorf("Expected URL %q, got %q", expect.URLs[i], url)
} }
} }
if k == "alpine-1.0.0" { for i, kw := range tt.Keywords {
if e.Name == "alpine" { if kw != expect.Keywords[i] {
if len(e.Chartfile.Keywords) == 4 { t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw)
alpine = true
}
} }
} }
} }
if !nginx {
t.Errorf("nginx entry was not decoded properly")
}
if !alpine {
t.Errorf("alpine entry was not decoded properly")
}
} }
func TestIndexDirectory(t *testing.T) { func TestIndexDirectory(t *testing.T) {
@ -124,21 +233,20 @@ func TestIndexDirectory(t *testing.T) {
// Other things test the entry generation more thoroughly. We just test a // Other things test the entry generation more thoroughly. We just test a
// few fields. // few fields.
cname := "frobnitz-1.2.3" cname := "frobnitz"
frob, ok := index.Entries[cname] frobs, ok := index.Entries[cname]
if !ok { if !ok {
t.Fatalf("Could not read chart %s", cname) t.Fatalf("Could not read chart %s", cname)
} }
frob := frobs[0]
if len(frob.Digest) == 0 { if len(frob.Digest) == 0 {
t.Errorf("Missing digest of file %s.", frob.Name) t.Errorf("Missing digest of file %s.", frob.Name)
} }
if frob.Chartfile == nil { if frob.URLs[0] != "http://localhost:8080/frobnitz-1.2.3.tgz" {
t.Fatalf("Chartfile %s not added to index.", cname) t.Errorf("Unexpected URLs: %v", frob.URLs)
}
if frob.URL != "http://localhost:8080/frobnitz-1.2.3.tgz" {
t.Errorf("Unexpected URL: %s", frob.URL)
} }
if frob.Chartfile.Name != "frobnitz" { if frob.Name != "frobnitz" {
t.Errorf("Expected frobnitz, got %q", frob.Chartfile.Name) t.Errorf("Expected frobnitz, got %q", frob.Name)
} }
} }

@ -23,21 +23,27 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/ghodss/yaml"
"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"
) )
var localRepoPath string var localRepoPath string
// StartLocalRepo starts a web server and serves files from the given path // StartLocalRepo starts a web server and serves files from the given path
func StartLocalRepo(path string) { func StartLocalRepo(path, address string) error {
fmt.Println("Now serving you on localhost:8879...") if address == "" {
address = ":8879"
}
localRepoPath = path localRepoPath = path
http.HandleFunc("/", rootHandler) http.HandleFunc("/", rootHandler)
http.HandleFunc("/charts/", indexHandler) http.HandleFunc("/charts/", indexHandler)
http.ListenAndServe(":8879", nil) return http.ListenAndServe(address, nil)
} }
func rootHandler(w http.ResponseWriter, r *http.Request) { func rootHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Welcome to the Kubernetes Package manager!\nBrowse charts on localhost:8879/charts!") fmt.Fprintf(w, "Welcome to the Kubernetes Package manager!\nBrowse charts on localhost:8879/charts!")
} }
func indexHandler(w http.ResponseWriter, r *http.Request) { func indexHandler(w http.ResponseWriter, r *http.Request) {
@ -81,9 +87,14 @@ func Reindex(ch *chart.Chart, path string) error {
} }
} }
if !found { if !found {
url := "localhost:8879/charts/" + name + ".tgz" dig, err := provenance.DigestFile(path)
if err != nil {
return err
}
y.Add(ch.Metadata, name+".tgz", "http://localhost:8879/charts", "sha256:"+dig)
out, err := y.addEntry(name, url) out, err := yaml.Marshal(y)
if err != nil { if err != nil {
return err return err
} }

@ -17,22 +17,24 @@ limitations under the License.
package repo // import "k8s.io/helm/pkg/repo" package repo // import "k8s.io/helm/pkg/repo"
import ( import (
"crypto/sha256"
"encoding/hex"
"errors" "errors"
"io" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v2" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil" "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 // ChartRepository represents a chart repository
type ChartRepository struct { type ChartRepository struct {
RootPath string RootPath string
@ -41,41 +43,109 @@ type ChartRepository struct {
IndexFile *IndexFile 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 // RepoFile represents the repositories.yaml file in $HELM_HOME
type RepoFile struct { type RepoFile struct {
Repositories map[string]string APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"`
Repositories []*Entry `json:"repositories"`
}
// NewRepoFile generates an empty repositories file.
//
// Generated and APIVersion are automatically set.
func NewRepoFile() *RepoFile {
return &RepoFile{
APIVersion: APIVersionV1,
Generated: time.Now(),
Repositories: []*Entry{},
}
} }
// LoadRepositoriesFile takes a file at the given path and returns a RepoFile object // LoadRepositoriesFile takes a file at the given path and returns a RepoFile object
//
// If this returns ErrRepoOutOfDate, it also returns a recovered RepoFile that
// can be saved as a replacement to the out of date file.
func LoadRepositoriesFile(path string) (*RepoFile, error) { func LoadRepositoriesFile(path string) (*RepoFile, error) {
b, err := ioutil.ReadFile(path) b, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var r RepoFile r := &RepoFile{}
err = yaml.Unmarshal(b, &r) err = yaml.Unmarshal(b, r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &r, nil // File is either corrupt, or is from before v2.0.0-Alpha.5
if r.APIVersion == "" {
m := map[string]string{}
if err = yaml.Unmarshal(b, &m); err != nil {
return nil, err
}
r := NewRepoFile()
for k, v := range m {
r.Add(&Entry{
Name: k,
URL: v,
Cache: fmt.Sprintf("%s-index.yaml", k),
})
}
return r, ErrRepoOutOfDate
}
return r, nil
} }
// UnmarshalYAML unmarshals the repo file // Add adds one or more repo entries to a repo file.
func (rf *RepoFile) UnmarshalYAML(unmarshal func(interface{}) error) error { func (r *RepoFile) Add(re ...*Entry) {
var repos map[string]string r.Repositories = append(r.Repositories, re...)
if err := unmarshal(&repos); err != nil { }
if _, ok := err.(*yaml.TypeError); !ok {
return err // Has returns true if the given name is already a repository name.
func (r *RepoFile) Has(name string) bool {
for _, rf := range r.Repositories {
if rf.Name == name {
return true
} }
} }
rf.Repositories = repos return false
return nil }
// Remove removes the entry from the list of repositories.
func (r *RepoFile) Remove(name string) bool {
cp := []*Entry{}
found := false
for _, rf := range r.Repositories {
if rf.Name == name {
found = true
continue
}
cp = append(cp, rf)
}
r.Repositories = cp
return found
}
// WriteFile writes a repositories file to the given path.
func (r *RepoFile) WriteFile(path string, perm os.FileMode) error {
data, err := yaml.Marshal(r)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, perm)
} }
// LoadChartRepository takes in a path to a local chart repository // LoadChartRepository loads a directory of charts as if it were a repository.
// which contains packaged charts and an index.yaml file //
// It requires the presence of an index.yaml file in the directory.
// //
// This function evaluates the contents of the directory and // This function evaluates the contents of the directory and
// returns a ChartRepository // returns a ChartRepository
@ -86,14 +156,17 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) {
} }
if !dirInfo.IsDir() { if !dirInfo.IsDir() {
return nil, errors.New(dir + "is not a directory") return nil, fmt.Errorf("%q is not a directory", dir)
} }
r := &ChartRepository{RootPath: dir, URL: url} 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 { filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() { if !f.IsDir() {
if strings.Contains(f.Name(), "index.yaml") { if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path) i, err := LoadIndexFile(path)
if err != nil { if err != nil {
return nil return nil
@ -109,82 +182,35 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) {
} }
func (r *ChartRepository) saveIndexFile() error { func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(&r.IndexFile.Entries) index, err := yaml.Marshal(r.IndexFile)
if err != nil { if err != nil {
return err return err
} }
return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644) return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644)
} }
// Index generates an index for the chart repository and writes an index.yaml file // Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error { func (r *ChartRepository) Index() error {
if r.IndexFile == nil { if r.IndexFile == nil {
r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)} r.IndexFile = NewIndexFile()
} }
existCharts := map[string]bool{}
for _, path := range r.ChartPaths { for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path) ch, err := chartutil.Load(path)
if err != nil { if err != nil {
return err return err
} }
chartfile := ch.Metadata digest, err := provenance.DigestFile(path)
digest, err := generateDigest(path)
if err != nil { if err != nil {
return err return err
} }
key := chartfile.Name + "-" + chartfile.Version if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
if r.IndexFile.Entries == nil { r.IndexFile.Add(ch.Metadata, path, r.URL, digest)
r.IndexFile.Entries = make(map[string]*ChartRef)
}
ref, ok := r.IndexFile.Entries[key]
var created string
if ok && ref.Created != "" {
created = ref.Created
} else {
created = nowString()
} }
// TODO: If a chart exists, but has a different Digest, should we error?
url, _ := url.Parse(r.URL)
url.Path = filepath.Join(url.Path, key+".tgz")
entry := &ChartRef{Chartfile: chartfile, Name: chartfile.Name, URL: url.String(), Created: created, Digest: digest, Removed: false}
r.IndexFile.Entries[key] = entry
// chart is existing
existCharts[key] = true
} }
r.IndexFile.SortEntries()
// update deleted charts with Removed = true
for k := range r.IndexFile.Entries {
if _, ok := existCharts[k]; !ok {
r.IndexFile.Entries[k].Removed = true
}
}
return r.saveIndexFile() return r.saveIndexFile()
} }
func nowString() string {
// FIXME: This is a different date format than we use elsewhere.
return time.Now().UTC().String()
}
func generateDigest(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
h := sha256.New()
io.Copy(h, f)
digest := h.Sum([]byte{})
return "sha256:" + hex.EncodeToString(digest[:]), nil
}

@ -22,12 +22,111 @@ import (
"reflect" "reflect"
"testing" "testing"
"time" "time"
"k8s.io/helm/pkg/proto/hapi/chart"
) )
const testRepositoriesFile = "testdata/repositories.yaml" const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository" const testRepository = "testdata/repository"
const testURL = "http://example-charts.com" const testURL = "http://example-charts.com"
func TestRepoFile(t *testing.T) {
rf := NewRepoFile()
rf.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
},
)
if len(rf.Repositories) != 2 {
t.Fatal("Expected 2 repositories")
}
if rf.Has("nosuchrepo") {
t.Error("Found nonexistent repo")
}
if !rf.Has("incubator") {
t.Error("incubator repo is missing")
}
stable := rf.Repositories[0]
if stable.Name != "stable" {
t.Error("stable is not named stable")
}
if stable.URL != "https://example.com/stable/charts" {
t.Error("Wrong URL for stable")
}
if stable.Cache != "stable-index.yaml" {
t.Error("Wrong cache name for stable")
}
}
func TestLoadRepositoriesFile(t *testing.T) {
expects := NewRepoFile()
expects.Add(
&Entry{
Name: "stable",
URL: "https://example.com/stable/charts",
Cache: "stable-index.yaml",
},
&Entry{
Name: "incubator",
URL: "https://example.com/incubator",
Cache: "incubator-index.yaml",
},
)
repofile, err := LoadRepositoriesFile(testRepositoriesFile)
if err != nil {
t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err)
}
if len(expects.Repositories) != len(repofile.Repositories) {
t.Fatalf("Unexpected repo data: %#v", repofile.Repositories)
}
for i, expect := range expects.Repositories {
got := repofile.Repositories[i]
if expect.Name != got.Name {
t.Errorf("Expected name %q, got %q", expect.Name, got.Name)
}
if expect.URL != got.URL {
t.Errorf("Expected url %q, got %q", expect.URL, got.URL)
}
if expect.Cache != got.Cache {
t.Errorf("Expected cache %q, got %q", expect.Cache, got.Cache)
}
}
}
func TestLoadPreV1RepositoriesFile(t *testing.T) {
r, err := LoadRepositoriesFile("testdata/old-repositories.yaml")
if err != nil && err != ErrRepoOutOfDate {
t.Fatal(err)
}
if len(r.Repositories) != 3 {
t.Fatalf("Expected 3 repos: %#v", r)
}
// Because they are parsed as a map, we lose ordering.
found := false
for _, rr := range r.Repositories {
if rr.Name == "best-charts-ever" {
found = true
}
}
if !found {
t.Errorf("expected the best charts ever. Got %#v", r.Repositories)
}
}
func TestLoadChartRepository(t *testing.T) { func TestLoadChartRepository(t *testing.T) {
cr, err := LoadChartRepository(testRepository, testURL) cr, err := LoadChartRepository(testRepository, testURL)
if err != nil { if err != nil {
@ -66,76 +165,94 @@ func TestIndex(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("Error loading index file %v", err) t.Errorf("Error loading index file %v", err)
} }
verifyIndex(t, actual)
entries := actual.Entries // Re-index and test again.
numEntries := len(entries) err = cr.Index()
if numEntries != 2 { if err != nil {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries) t.Errorf("Error performing re-index: %s\n", err)
}
timestamps := make(map[string]string)
var empty time.Time
for chartName, details := range entries {
if details == nil {
t.Errorf("Chart Entry is not filled out for %s", chartName)
}
if details.Created == empty.String() {
t.Errorf("Created timestamp under %s chart entry is nil", chartName)
}
timestamps[chartName] = details.Created
if details.Digest == "" {
t.Errorf("Digest was not set for %s", chartName)
}
}
if err = cr.Index(); err != nil {
t.Errorf("Error performing index the second time: %v\n", err)
} }
second, err := LoadIndexFile(tempIndexPath) second, err := LoadIndexFile(tempIndexPath)
if err != nil { if err != nil {
t.Errorf("Error loading index file second time: %#v\n", err) t.Errorf("Error re-loading index file %v", err)
} }
verifyIndex(t, second)
}
for chart, created := range timestamps { func verifyIndex(t *testing.T, actual *IndexFile) {
v, ok := second.Entries[chart]
if !ok { var empty time.Time
t.Errorf("Expected %s chart entry in index file but did not find it", chart) if actual.Generated == empty {
} t.Errorf("Generated should be greater than 0: %s", actual.Generated)
if v.Created != created {
t.Errorf("Expected Created timestamp to be %s, but got %s for chart %s", created, v.Created, chart)
}
// Created manually since we control the input of the test
expectedURL := testURL + "/" + chart + ".tgz"
if v.URL != expectedURL {
t.Errorf("Expected url in entry to be %s but got %s for chart: %s", expectedURL, v.URL, chart)
}
} }
}
func TestLoadRepositoriesFile(t *testing.T) { if actual.APIVersion != APIVersionV1 {
rf, err := LoadRepositoriesFile(testRepositoriesFile) t.Error("Expected v1 API")
if err != nil {
t.Errorf(testRepositoriesFile + " could not be loaded: " + err.Error())
} }
expected := map[string]string{"best-charts-ever": "http://best-charts-ever.com",
"okay-charts": "http://okay-charts.org", "example123": "http://examplecharts.net/charts/123"}
numOfRepositories := len(rf.Repositories) entries := actual.Entries
expectedNumOfRepositories := 3 if numEntries := len(entries); numEntries != 2 {
if numOfRepositories != expectedNumOfRepositories { t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
t.Errorf("Expected %v repositories but only got %v", expectedNumOfRepositories, numOfRepositories)
} }
for expectedRepo, expectedURL := range expected { expects := map[string]ChartVersions{
actual, ok := rf.Repositories[expectedRepo] "frobnitz": {
{
Metadata: &chart.Metadata{
Name: "frobnitz",
Version: "1.2.3",
},
},
},
"sprocket": {
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.2.0",
},
},
},
}
for name, versions := range expects {
got, ok := entries[name]
if !ok { if !ok {
t.Errorf("Expected repository: %v but was not found", expectedRepo) t.Errorf("Could not find %q entry", name)
continue
} }
if len(versions) != len(got) {
if expectedURL != actual { t.Errorf("Expected %d versions, got %d", len(versions), len(got))
t.Errorf("Expected url %s for the %s repository but got %s ", expectedURL, expectedRepo, actual) 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")
}
} }
} }
} }

@ -82,20 +82,27 @@ func (s *Server) CopyCharts(origin string) ([]string, error) {
copied[i] = newname copied[i] = newname
} }
err = s.CreateIndex()
return copied, err
}
// CreateIndex will read docroot and generate an index.yaml file.
func (s *Server) CreateIndex() error {
// generate the index // generate the index
index, err := repo.IndexDirectory(s.docroot, s.URL()) index, err := repo.IndexDirectory(s.docroot, s.URL())
if err != nil { if err != nil {
return copied, err return err
} }
d, err := yaml.Marshal(index.Entries) d, err := yaml.Marshal(index)
if err != nil { if err != nil {
return copied, err return err
} }
println(string(d))
ifile := filepath.Join(s.docroot, "index.yaml") ifile := filepath.Join(s.docroot, "index.yaml")
err = ioutil.WriteFile(ifile, d, 0755) return ioutil.WriteFile(ifile, d, 0755)
return copied, err
} }
func (s *Server) start() { func (s *Server) start() {
@ -119,12 +126,9 @@ func (s *Server) URL() string {
// 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(helmhome, name, url string) error {
// Oddly, there is no repo.Save function for this. rf := repo.NewRepoFile()
data, err := yaml.Marshal(&map[string]string{name: url}) rf.Add(&repo.Entry{Name: name, URL: url})
if err != nil {
return err
}
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
dest := filepath.Join(helmhome, "repository/repositories.yaml") dest := filepath.Join(helmhome, "repository/repositories.yaml")
return ioutil.WriteFile(dest, data, 0666) return rf.WriteFile(dest, 0644)
} }

@ -22,7 +22,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gopkg.in/yaml.v2" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
@ -77,23 +77,20 @@ func TestServer(t *testing.T) {
return return
} }
var m map[string]*repo.ChartRef m := repo.NewIndexFile()
if err := yaml.Unmarshal(data, &m); err != nil { if err := yaml.Unmarshal(data, m); err != nil {
t.Error(err) t.Error(err)
return return
} }
if l := len(m); l != 1 { if l := len(m.Entries); l != 1 {
t.Errorf("Expected 1 entry, got %d", l) t.Errorf("Expected 1 entry, got %d", l)
return return
} }
expect := "examplechart-0.1.0" expect := "examplechart"
if m[expect].Name != "examplechart-0.1.0" { if !m.Has(expect, "0.1.0") {
t.Errorf("Unexpected chart: %s", m[expect].Name) t.Errorf("missing %q", expect)
}
if m[expect].Chartfile.Name != "examplechart" {
t.Errorf("Unexpected chart: %s", m[expect].Chartfile.Name)
} }
res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing")

@ -1,19 +1,32 @@
nginx-0.1.0: apiVersion: v1
url: http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz entries:
name: nginx nginx:
chartfile: - urls:
- http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz
name: nginx name: nginx
description: string description: string
version: 0.1.0 version: 0.1.0
home: https://github.com/something home: https://github.com/something
digest: "sha256:1234567890abcdef"
keywords: keywords:
- popular - popular
- web server - web server
- proxy - proxy
alpine-1.0.0: - urls:
url: http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz - http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz
name: alpine name: nginx
chartfile: description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
alpine:
- urls:
- http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine name: alpine
description: string description: string
version: 1.0.0 version: 1.0.0
@ -23,4 +36,5 @@ alpine-1.0.0:
- alpine - alpine
- small - small
- sumtin - sumtin
digest: "sha256:1234567890abcdef"

@ -0,0 +1,3 @@
best-charts-ever: http://best-charts-ever.com
okay-charts: http://okay-charts.org
example123: http://examplecharts.net/charts/123

@ -1,3 +1,8 @@
best-charts-ever: http://best-charts-ever.com apiVersion: v1
okay-charts: http://okay-charts.org repositories:
example123: http://examplecharts.net/charts/123 - name: stable
url: https://example.com/stable/charts
cache: stable-index.yaml
- name: incubator
url: https://example.com/incubator
cache: incubator-index.yaml

Loading…
Cancel
Save