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
test-unit:
$(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS)
HELM_HOME=/no/such/dir $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS)
.PHONY: test-style
test-style:

@ -30,7 +30,7 @@ import (
func TestDependencyBuildCmd(t *testing.T) {
oldhome := helmHome
hh, err := tempHelmHome()
hh, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}
@ -108,8 +108,12 @@ func TestDependencyBuildCmd(t *testing.T) {
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)
}
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) {
// Set up a testing helm home
oldhome := helmHome
hh, err := tempHelmHome()
hh, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}
@ -90,7 +90,8 @@ func TestDependencyUpdateCmd(t *testing.T) {
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)
}

@ -69,7 +69,7 @@ type ChartDownloader struct {
// For VerifyNever and VerifyIfPossible, the Verification may be empty.
func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) {
// resolve URL
u, err := c.ResolveChartRef(ref)
u, err := c.ResolveChartVersion(ref)
if err != nil {
return nil, err
}
@ -111,10 +111,10 @@ func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verif
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.
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.
u, err := url.ParseRequestURI(ref)
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 {
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())
@ -133,15 +133,29 @@ func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) {
// See if it's of the form: repo/path_to_chart
p := strings.Split(ref, "/")
if len(p) > 1 {
if baseURL, ok := r.Repositories[p[0]]; ok {
if !strings.HasSuffix(baseURL, "/") {
baseURL = baseURL + "/"
}
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/"))
rf, err := findRepoEntry(p[0], r.Repositories)
if err != nil {
return u, err
}
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.

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

@ -175,8 +175,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
for _, dep := range deps {
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(target, dep.Repository, repos)
churl, err := findChartURL(dep.Name, dep.Repository, repos)
if err != nil {
fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err)
continue
@ -207,7 +206,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
found = true
} else {
for _, repo := range repos {
if urlsAreEqual(repo, dd.Repository) {
if urlsAreEqual(repo.URL, dd.Repository) {
found = true
}
}
@ -236,25 +235,23 @@ func (m *Manager) UpdateRepositories() error {
return nil
}
func (m *Manager) parallelRepoUpdate(repos map[string]string) {
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) {
out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
for name, url := range repos {
for _, re := range repos {
wg.Add(1)
go func(n, u string) {
err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n))
if err != nil {
updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository: %s", n, err)
fmt.Fprintln(out, updateErr)
if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n)
}
wg.Done()
}(name, url)
}(re.Name, re.URL)
}
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.
@ -280,7 +277,15 @@ func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository)
if urlsAreEqual(repourl, cr.URL) {
for ename, entry := range cr.IndexFile.Entries {
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)
}
// localName: chartRepo
for lname, url := range rf.Repositories {
for _, re := range rf.Repositories {
lname := re.Name
cacheindex := m.HelmHome.CacheIndex(lname)
index, err := repo.LoadIndexFile(cacheindex)
if err != nil {
@ -311,7 +316,7 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
}
cr := &repo.ChartRepository{
URL: url,
URL: re.URL,
IndexFile: index,
}
indices[lname] = cr

@ -1,38 +1,46 @@
alpine-0.1.0:
name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
created: 2016-09-06 21:58:44.211261566 +0000 UTC
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
chartfile:
name: alpine
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
mariadb-0.3.0:
name: mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
created: 2016-09-06 21:58:44.211870222 +0000 UTC
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
chartfile:
name: mariadb
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
engine: gotpl
icon: ""
apiVersion: v1
entries:
alpine:
- name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
- name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.2.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
mariadb:
- name: mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- 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) {
hh, err := tempHelmHome()
hh, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}

@ -22,17 +22,20 @@ import (
"io"
"io/ioutil"
"math/rand"
"os"
"regexp"
"testing"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/proto/hapi/version"
"k8s.io/helm/pkg/repo"
)
var mockHookTemplate = `apiVersion: v1
@ -211,11 +214,11 @@ type releaseCase struct {
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.
// You must also set helmHome yourself.
func tempHelmHome() (string, error) {
func tempHelmHome(t *testing.T) (string, error) {
oldhome := helmHome
dir, err := ioutil.TempDir("", "helm_home-")
if err != nil {
@ -223,9 +226,57 @@ func tempHelmHome() (string, error) {
}
helmHome = dir
if err := ensureHome(); err != nil {
if err := ensureTestHome(helmpath.Home(helmHome), t); err != nil {
return "n/", err
}
helmHome = oldhome
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.
//
// 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"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/cmd/helm/installer"
"k8s.io/helm/pkg/repo"
)
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/)
`
var (
defaultRepository = "stable"
defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts"
const (
stableRepository = "stable"
localRepository = "local"
stableRepositoryURL = "http://storage.googleapis.com/kubernetes-charts"
localRepositoryURL = "http://localhost:8879/charts"
)
type initCmd struct {
image string
clientOnly bool
out io.Writer
home helmpath.Home
}
func newInitCmd(out io.Writer) *cobra.Command {
@ -56,6 +61,7 @@ func newInitCmd(out io.Writer) *cobra.Command {
if len(args) != 0 {
return errors.New("This command does not accept arguments")
}
i.home = helmpath.Home(homePath())
return i.run()
},
}
@ -66,7 +72,7 @@ func newInitCmd(out io.Writer) *cobra.Command {
// runInit initializes local config and installs tiller to Kubernetes Cluster
func (i *initCmd) run() error {
if err := ensureHome(); err != nil {
if err := ensureHome(i.home, i.out); err != nil {
return err
}
@ -101,12 +107,11 @@ func requireHome() error {
// ensureHome checks to see if $HELM_HOME exists
//
// If $HELM_HOME does not exist, this function will create it.
func ensureHome() error {
configDirectories := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()}
func ensureHome(home helmpath.Home, out io.Writer) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository()}
for _, p := range configDirectories {
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 {
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 {
fmt.Printf("Creating %s \n", repoFile)
if _, err := os.Create(repoFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", repoFile)
r := repo.NewRepoFile()
r.Add(&repo.Entry{
Name: stableRepository,
URL: stableRepositoryURL,
Cache: "stable-index.yaml",
}, &repo.Entry{
Name: localRepository,
URL: localRepositoryURL,
Cache: "local-index.yaml",
})
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
if err := addRepository(defaultRepository, defaultRepositoryURL); err != nil {
return err
cif := home.CacheIndex(stableRepository)
if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil {
fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm update')\n", stableRepository, 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 {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
}
localRepoIndexFile := localRepoDirectory(localRepoIndexFilePath)
if fi, err := os.Stat(localRepoIndexFile); err != nil {
fmt.Printf("Creating %s \n", localRepoIndexFile)
_, err := os.Create(localRepoIndexFile)
if err != nil {
fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile)
i := repo.NewIndexFile()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err
}
@ -142,6 +164,6 @@ func ensureHome() error {
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
}

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

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/provenance"
@ -50,6 +51,7 @@ type packageCmd struct {
key string
keyring string
out io.Writer
home helmpath.Home
}
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",
Long: packageDesc,
RunE: func(cmd *cobra.Command, args []string) error {
pkg.home = helmpath.Home(homePath())
if len(args) == 0 {
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)
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
// the case where we saved here, but didn't save to the default destination.
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
} 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"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
)
func TestPackage(t *testing.T) {
@ -57,6 +59,13 @@ func TestPackage(t *testing.T) {
expect: "keyring is required for signing a package",
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",
args: []string{"testdata/testcharts/alpine"},
@ -87,7 +96,11 @@ func TestPackage(t *testing.T) {
t.Fatal(err)
}
ensureTestHome(helmpath.Home(tmp), t)
oldhome := homePath()
helmHome = tmp
defer func() {
helmHome = oldhome
os.Chdir(origDir)
os.RemoveAll(tmp)
}()

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

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

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

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

@ -17,45 +17,52 @@ limitations under the License.
package main
import (
"bytes"
"os"
"strings"
"testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
)
func TestRepoRemove(t *testing.T) {
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)
}
if err := insertRepoLine(testName, testURL); err != nil {
t.Errorf("%s", err)
if err := insertRepoLine(testName, testURL, hh); err != nil {
t.Error(err)
}
mf, _ := os.Create(cacheIndexFile(testName))
mf, _ := os.Create(hh.CacheIndex(testName))
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)
}
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)
}
f, err := repo.LoadRepositoriesFile(repositoriesFile())
f, err := repo.LoadRepositoriesFile(hh.RepositoryFile())
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)
}
}

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
)
@ -37,8 +38,9 @@ future releases.
type repoUpdateCmd struct {
repoFile string
update func(map[string]string, bool, io.Writer)
update func([]*repo.Entry, bool, io.Writer, helmpath.Home)
out io.Writer
home helmpath.Home
}
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",
Long: updateDesc,
RunE: func(cmd *cobra.Command, args []string) error {
u.home = helmpath.Home(homePath())
return u.run()
},
}
@ -69,29 +72,29 @@ func (u *repoUpdateCmd) run() error {
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
}
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...")
var wg sync.WaitGroup
for name, url := range repos {
for _, re := range repos {
wg.Add(1)
go func(n, u string) {
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 {
updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository", n)
if verbose {
updateErr = updateErr + ": " + err.Error()
}
fmt.Fprintln(out, updateErr)
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n)
}
}(name, url)
}(re.Name, re.URL)
}
wg.Wait()
fmt.Fprintln(out, "Update Complete. Happy Helming!")
fmt.Fprintln(out, "Update Complete. Happy Helming!")
}

@ -19,19 +19,34 @@ import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
)
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)
// Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos map[string]string, verbose bool, out io.Writer) {
for name := range repos {
fmt.Fprintln(out, name)
updater := func(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) {
for _, re := range repos {
fmt.Fprintln(out, re.Name)
}
}
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) {
// This tests the repo in isolation. It creates a mock HTTP server that simply
// returns a static YAML file in the anticipate format.
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(mockRepoIndex))
})
srv := httptest.NewServer(handler)
defer srv.Close()
srv := repotest.NewServer("testdata/testserver")
defer srv.Stop()
thome, err := tempHelmHome(t)
if err != nil {
t.Fatal(err)
}
oldhome := homePath()
helmHome = thome
defer func() {
helmHome = oldhome
os.Remove(thome)
}()
buf := bytes.NewBuffer(nil)
repos := map[string]string{
"charts": srv.URL,
repos := []*repo.Entry{
{Name: "charts", URL: srv.URL()},
}
updateCharts(repos, false, buf)
updateCharts(repos, false, buf, helmpath.Home(thome))
got := buf.String()
if strings.Contains(got, "Unable to get an update") {

@ -101,11 +101,12 @@ func (s *searchCmd) buildIndex() (*search.Index, error) {
}
i := search.NewIndex()
for n := range rf.Repositories {
for _, re := range rf.Repositories {
n := re.Name
f := s.helmhome.CacheIndex(n)
ind, err := repo.LoadIndexFile(f)
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
}

@ -44,27 +44,34 @@ type Result struct {
// Index is a searchable index of chart information.
type Index struct {
lines map[string]string
charts map[string]*repo.ChartRef
charts map[string]*repo.ChartVersion
}
const sep = "\v"
// NewIndex creats a new 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.
func (i *Index) AddRepo(rname string, ind *repo.IndexFile) {
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)
i.lines[fname] = indstr(rname, ref)
i.charts[fname] = ref
i.lines[fname] = indstr(rname, ref[0])
i.charts[fname] = ref[0]
}
}
// 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
}
@ -136,7 +143,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
}
// 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]
if !ok {
return nil, errors.New("no such chart")
@ -174,10 +181,8 @@ func (s scoreSorter) Less(a, b int) bool {
return first.Name < second.Name
}
func indstr(name string, ref *repo.ChartRef) string {
i := ref.Name + sep + name + "/" + ref.Name + sep
if ref.Chartfile != nil {
i += ref.Chartfile.Description + sep + strings.Join(ref.Chartfile.Keywords, sep)
}
func indstr(name string, ref *repo.ChartVersion) string {
i := ref.Name + sep + name + "/" + ref.Name + sep +
ref.Description + sep + strings.Join(ref.Keywords, " ")
return strings.ToLower(i)
}

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

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

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

@ -1,54 +1,46 @@
alpine-0.1.0:
name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
created: 2016-09-06 21:58:44.211261566 +0000 UTC
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
chartfile:
name: alpine
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
alpine-0.2.0:
name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
created: 2016-09-06 21:58:44.211261566 +0000 UTC
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
chartfile:
name: alpine
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.2.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
mariadb-0.3.0:
name: mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
created: 2016-09-06 21:58:44.211870222 +0000 UTC
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
chartfile:
name: mariadb
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
engine: gotpl
icon: ""
apiVersion: v1
entries:
alpine:
- name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
- name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.2.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.2.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
mariadb:
- name: mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- mariadb
- 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
local: http://localhost:8879/charts
apiVersion: v1
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:
```
alpine-0.1.0:
name: alpine
url: https://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
created: 2016-05-26 11:23:44.086354411 +0000 UTC
digest: sha256:78e9a4282295184e8ce1496d23987993673f38e33e203c8bc18bc838a73e5864
chartfile:
name: alpine
description: Deploy a basic Alpine Linux pod
version: 0.1.0
home: https://github.com/example-charts/alpine
redis-2.0.0:
name: redis
url: https://storage.googleapis.com/kubernetes-charts/redis-2.0.0.tgz
created: 2016-05-26 11:23:44.087939192 +0000 UTC
digest: sha256:bde9c2949e64d059c18d8f93566a64dafc6d2e8e259a70322fb804831dfd0b5b
chartfile:
name: redis
description: Port of the replicatedservice template from kubernetes/charts
version: 2.0.0
home: https://github.com/example-charts/redis
apiVersion: v1
entries:
nginx:
- urls:
- http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz
name: nginx
description: string
version: 0.1.0
home: https://github.com/something
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
- urls:
- http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz
name: nginx
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
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.

@ -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
import (
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"gopkg.in/yaml.v2"
"github.com/Masterminds/semver"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
@ -31,39 +36,116 @@ import (
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
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.
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
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
name := strings.TrimSuffix(filename, ".tgz")
cr := &ChartRef{
Name: name,
URL: baseURL + "/" + filename,
Chartfile: md,
Digest: digest,
Created: nowString(),
}
i.Entries[name] = cr
cr := &ChartVersion{
URLs: []string{baseURL + "/" + filename},
Metadata: md,
Digest: digest,
Created: time.Now(),
}
if ee, ok := i.Entries[md.Name]; !ok {
i.Entries[md.Name] = ChartVersions{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
// ChartRef represents a chart entry in the IndexFile
type ChartRef struct {
Name string `yaml:"name" json:"name"`
URL string `yaml:"url" json:"url"`
Created string `yaml:"created,omitempty" json:"created,omitempty"`
Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
Chartfile *chart.Metadata `yaml:"chartfile" json:"chartfile"`
// ChartVersion represents a chart entry in the IndexFile
type ChartVersion struct {
*chart.Metadata
URLs []string `yaml:"url" json:"urls"`
Created time.Time `yaml:"created,omitempty" json:"created,omitempty"`
Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
}
// IndexDirectory reads a (flat) directory and generates an index.
@ -104,42 +186,30 @@ func DownloadIndexFile(repoName, url, indexFilePath string) error {
}
defer resp.Body.Close()
var r IndexFile
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if err := yaml.Unmarshal(b, &r); err != nil {
if _, err := LoadIndex(b); err != nil {
return err
}
return ioutil.WriteFile(indexFilePath, b, 0644)
}
// UnmarshalYAML unmarshals the index file
func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var refs map[string]*ChartRef
if err := unmarshal(&refs); err != nil {
return err
}
i.Entries = refs
return nil
}
func (i *IndexFile) addEntry(name string, url string) ([]byte, error) {
if i.Entries == nil {
i.Entries = make(map[string]*ChartRef)
// LoadIndex loads an index file and does minimal validity checking.
//
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func LoadIndex(data []byte) (*IndexFile, error) {
i := &IndexFile{}
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
}
entry := ChartRef{Name: name, URL: url}
i.Entries[name] = &entry
out, err := yaml.Marshal(&i.Entries)
if err != nil {
return nil, err
if i.APIVersion == "" {
return i, ErrNoAPIVersion
}
return out, nil
return i, nil
}
// 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 {
return nil, err
}
indexfile := NewIndexFile()
err = yaml.Unmarshal(b, indexfile)
if err != nil {
return nil, err
}
return indexfile, nil
return LoadIndex(b)
}

@ -17,7 +17,6 @@ limitations under the License.
package repo
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -25,25 +24,74 @@ import (
"path/filepath"
"testing"
"gopkg.in/yaml.v2"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const testfile = "testdata/local-index.yaml"
var (
const (
testfile = "testdata/local-index.yaml"
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) {
fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml")
if err != nil {
t.Errorf("%#v", err)
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "binary/octet-stream")
fmt.Fprintln(w, string(fileBytes))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(fileBytes)
}))
defer srv.Close()
dirName, err := ioutil.TempDir("", "tmp")
if err != nil {
@ -52,7 +100,7 @@ func TestDownloadIndexFile(t *testing.T) {
defer os.RemoveAll(dirName)
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)
}
@ -65,50 +113,111 @@ func TestDownloadIndexFile(t *testing.T) {
t.Errorf("error reading index file: %#v", err)
}
var i IndexFile
if err = yaml.Unmarshal(b, &i); err != nil {
t.Errorf("error unmarshaling index file: %#v", err)
i, err := LoadIndex(b)
if err != nil {
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)
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) {
cf, err := LoadIndexFile(testfile)
if err != nil {
t.Errorf("Failed to load index file: %s", err)
}
if len(cf.Entries) != 2 {
t.Errorf("Expected 2 entries in the index file, but got %d", len(cf.Entries))
}
nginx := false
alpine := false
for k, e := range cf.Entries {
if k == "nginx-0.1.0" {
if e.Name == "nginx" {
if len(e.Chartfile.Keywords) == 3 {
nginx = true
}
alpine, ok := i.Entries["alpine"]
if !ok {
t.Errorf("'alpine' section not found.")
return
}
if l := len(alpine); l != 1 {
t.Errorf("'alpine' should have 1 chart, got %d", l)
return
}
nginx, ok := i.Entries["nginx"]
if !ok || len(nginx) != 2 {
t.Error("Expected 2 nginx entries")
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" {
if e.Name == "alpine" {
if len(e.Chartfile.Keywords) == 4 {
alpine = true
}
for i, kw := range tt.Keywords {
if kw != expect.Keywords[i] {
t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw)
}
}
}
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) {
@ -124,21 +233,20 @@ func TestIndexDirectory(t *testing.T) {
// Other things test the entry generation more thoroughly. We just test a
// few fields.
cname := "frobnitz-1.2.3"
frob, ok := index.Entries[cname]
cname := "frobnitz"
frobs, ok := index.Entries[cname]
if !ok {
t.Fatalf("Could not read chart %s", cname)
}
frob := frobs[0]
if len(frob.Digest) == 0 {
t.Errorf("Missing digest of file %s.", frob.Name)
}
if frob.Chartfile == nil {
t.Fatalf("Chartfile %s not added to index.", cname)
}
if frob.URL != "http://localhost:8080/frobnitz-1.2.3.tgz" {
t.Errorf("Unexpected URL: %s", frob.URL)
if frob.URLs[0] != "http://localhost:8080/frobnitz-1.2.3.tgz" {
t.Errorf("Unexpected URLs: %v", frob.URLs)
}
if frob.Chartfile.Name != "frobnitz" {
t.Errorf("Expected frobnitz, got %q", frob.Chartfile.Name)
if frob.Name != "frobnitz" {
t.Errorf("Expected frobnitz, got %q", frob.Name)
}
}

@ -23,21 +23,27 @@ import (
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance"
)
var localRepoPath string
// StartLocalRepo starts a web server and serves files from the given path
func StartLocalRepo(path string) {
fmt.Println("Now serving you on localhost:8879...")
func StartLocalRepo(path, address string) error {
if address == "" {
address = ":8879"
}
localRepoPath = path
http.HandleFunc("/", rootHandler)
http.HandleFunc("/charts/", indexHandler)
http.ListenAndServe(":8879", nil)
return http.ListenAndServe(address, nil)
}
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!")
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
@ -81,9 +87,14 @@ func Reindex(ch *chart.Chart, path string) error {
}
}
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 {
return err
}

@ -17,22 +17,24 @@ limitations under the License.
package repo // import "k8s.io/helm/pkg/repo"
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"gopkg.in/yaml.v2"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
)
// ErrRepoOutOfDate indicates that the repository file is out of date, but
// is fixable.
var ErrRepoOutOfDate = errors.New("repository file is out of date")
// ChartRepository represents a chart repository
type ChartRepository struct {
RootPath string
@ -41,41 +43,109 @@ type ChartRepository struct {
IndexFile *IndexFile
}
// Entry represents one repo entry in a repositories listing.
type Entry struct {
Name string `json:"name"`
Cache string `json:"cache"`
URL string `json:"url"`
}
// RepoFile represents the repositories.yaml file in $HELM_HOME
type RepoFile struct {
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
//
// 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) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
var r RepoFile
err = yaml.Unmarshal(b, &r)
r := &RepoFile{}
err = yaml.Unmarshal(b, r)
if err != nil {
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
func (rf *RepoFile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var repos map[string]string
if err := unmarshal(&repos); err != nil {
if _, ok := err.(*yaml.TypeError); !ok {
return err
// Add adds one or more repo entries to a repo file.
func (r *RepoFile) Add(re ...*Entry) {
r.Repositories = append(r.Repositories, re...)
}
// 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 nil
return false
}
// 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
// which contains packaged charts and an index.yaml file
// LoadChartRepository loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
//
// This function evaluates the contents of the directory and
// returns a ChartRepository
@ -86,14 +156,17 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) {
}
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}
// FIXME: Why are we recursively walking directories?
// FIXME: Why are we not reading the repositories.yaml to figure out
// what repos to use?
filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
if strings.Contains(f.Name(), "index.yaml") {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
@ -109,82 +182,35 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) {
}
func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(&r.IndexFile.Entries)
index, err := yaml.Marshal(r.IndexFile)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file
// Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error {
if r.IndexFile == nil {
r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)}
r.IndexFile = NewIndexFile()
}
existCharts := map[string]bool{}
for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path)
if err != nil {
return err
}
chartfile := ch.Metadata
digest, err := generateDigest(path)
digest, err := provenance.DigestFile(path)
if err != nil {
return err
}
key := chartfile.Name + "-" + chartfile.Version
if r.IndexFile.Entries == nil {
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()
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.URL, digest)
}
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
// TODO: If a chart exists, but has a different Digest, should we error?
}
// update deleted charts with Removed = true
for k := range r.IndexFile.Entries {
if _, ok := existCharts[k]; !ok {
r.IndexFile.Entries[k].Removed = true
}
}
r.IndexFile.SortEntries()
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"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository"
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) {
cr, err := LoadChartRepository(testRepository, testURL)
if err != nil {
@ -66,76 +165,94 @@ func TestIndex(t *testing.T) {
if err != nil {
t.Errorf("Error loading index file %v", err)
}
verifyIndex(t, actual)
entries := actual.Entries
numEntries := len(entries)
if numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
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)
// Re-index and test again.
err = cr.Index()
if err != nil {
t.Errorf("Error performing re-index: %s\n", err)
}
second, err := LoadIndexFile(tempIndexPath)
if err != nil {
t.Errorf("Error 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 {
v, ok := second.Entries[chart]
if !ok {
t.Errorf("Expected %s chart entry in index file but did not find it", chart)
}
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 verifyIndex(t *testing.T, actual *IndexFile) {
var empty time.Time
if actual.Generated == empty {
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
}
}
func TestLoadRepositoriesFile(t *testing.T) {
rf, err := LoadRepositoriesFile(testRepositoriesFile)
if err != nil {
t.Errorf(testRepositoriesFile + " could not be loaded: " + err.Error())
if actual.APIVersion != APIVersionV1 {
t.Error("Expected v1 API")
}
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)
expectedNumOfRepositories := 3
if numOfRepositories != expectedNumOfRepositories {
t.Errorf("Expected %v repositories but only got %v", expectedNumOfRepositories, numOfRepositories)
entries := actual.Entries
if numEntries := len(entries); numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
for expectedRepo, expectedURL := range expected {
actual, ok := rf.Repositories[expectedRepo]
expects := map[string]ChartVersions{
"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 {
t.Errorf("Expected repository: %v but was not found", expectedRepo)
t.Errorf("Could not find %q entry", name)
continue
}
if expectedURL != actual {
t.Errorf("Expected url %s for the %s repository but got %s ", expectedURL, expectedRepo, actual)
if len(versions) != len(got) {
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
continue
}
for i, e := range versions {
g := got[i]
if e.Name != g.Name {
t.Errorf("Expected %q, got %q", e.Name, g.Name)
}
if e.Version != g.Version {
t.Errorf("Expected %q, got %q", e.Version, g.Version)
}
if len(g.Keywords) != 3 {
t.Error("Expected 3 keyrwords.")
}
if len(g.Maintainers) != 2 {
t.Error("Expected 2 maintainers.")
}
if g.Created == empty {
t.Error("Expected created to be non-empty")
}
if g.Description == "" {
t.Error("Expected description to be non-empty")
}
if g.Home == "" {
t.Error("Expected home to be non-empty")
}
if g.Digest == "" {
t.Error("Expected digest to be non-empty")
}
if len(g.URLs) != 1 {
t.Error("Expected exactly 1 URL")
}
}
}
}

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

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

@ -1,19 +1,32 @@
nginx-0.1.0:
url: http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz
name: nginx
chartfile:
apiVersion: v1
entries:
nginx:
- urls:
- http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz
name: nginx
description: string
version: 0.1.0
home: https://github.com/something
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
alpine-1.0.0:
url: http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine
chartfile:
- urls:
- http://storage.googleapis.com/kubernetes-charts/nginx-0.2.0.tgz
name: nginx
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
description: string
version: 1.0.0
@ -23,4 +36,5 @@ alpine-1.0.0:
- alpine
- small
- 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
okay-charts: http://okay-charts.org
example123: http://examplecharts.net/charts/123
apiVersion: v1
repositories:
- 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