Merge pull request #1293 from technosophos/feat/1198-no-version-required

feat(helm): remove the requirement that fetch/install need version
pull/1299/head
Matt Butcher 9 years ago committed by GitHub
commit 2011788da9

@ -32,6 +32,8 @@ Think of it like apt/yum/homebrew for Kubernetes.
Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`. Download a [release tarball of helm for your platform](https://github.com/kubernetes/helm/releases). Unpack the `helm` binary and add it to your PATH and you are good to go! OS X/[Cask](https://caskroom.github.io/) users can `brew cask install helm`.
To rapidly get Helm up and running, start with the [Quick Start Guide](docs/quickstart.md).
See the [installation guide](docs/install.md) for more options, See the [installation guide](docs/install.md) for more options,
including installing pre-releases. including installing pre-releases.

@ -24,6 +24,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -67,21 +68,24 @@ type ChartDownloader struct {
// If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails. // If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
// //
// 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) { //
// Returns a string path to the location where the file was downloaded and a verification
// (if provenance was verified), or an error if something bad happened.
func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
// resolve URL // resolve URL
u, err := c.ResolveChartVersion(ref) u, err := c.ResolveChartVersion(ref, version)
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
data, err := download(u.String()) data, err := download(u.String())
if err != nil { if err != nil {
return nil, err return "", nil, err
} }
name := filepath.Base(u.Path) name := filepath.Base(u.Path)
destfile := filepath.Join(dest, name) destfile := filepath.Join(dest, name)
if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil { if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil {
return nil, err return destfile, nil, err
} }
// If provenance is requested, verify it. // If provenance is requested, verify it.
@ -91,31 +95,40 @@ func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verif
body, err := download(u.String() + ".prov") body, err := download(u.String() + ".prov")
if err != nil { if err != nil {
if c.Verify == VerifyAlways { if c.Verify == VerifyAlways {
return ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov")
} }
fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err)
return ver, nil return destfile, ver, nil
} }
provfile := destfile + ".prov" provfile := destfile + ".prov"
if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil { if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil {
return nil, err return destfile, nil, err
} }
ver, err = VerifyChart(destfile, c.Keyring) ver, err = VerifyChart(destfile, c.Keyring)
if err != nil { if err != nil {
// Fail always in this case, since it means the verification step // Fail always in this case, since it means the verification step
// failed. // failed.
return ver, err return destfile, ver, err
} }
} }
return ver, nil return destfile, ver, nil
} }
// ResolveChartVersion 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) ResolveChartVersion(ref string) (*url.URL, error) { //
// A version is a SemVer string (1.2.3-beta.1+f334a6789).
//
// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
// - For a chart reference
// * If version is non-empty, this will return the URL for that version
// * If version is empty, this will return the URL for the latest version
// * If no version can be found, an error is returned
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) {
// See if it's already a full URL. // See if it's already a full URL.
// FIXME: Why do we use url.ParseRequestURI instead of url.Parse?
u, err := url.ParseRequestURI(ref) u, err := url.ParseRequestURI(ref)
if err == nil { if err == nil {
// If it has a scheme and host and path, it's a full URL // If it has a scheme and host and path, it's a full URL
@ -131,22 +144,54 @@ func (c *ChartDownloader) ResolveChartVersion(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.SplitN(ref, "/", 2)
if len(p) > 1 { if len(p) < 2 {
rf, err := findRepoEntry(p[0], r.Repositories) return u, fmt.Errorf("invalid chart url format: %s", ref)
if err != nil { }
return u, err
} repoName := p[0]
if rf.URL == "" { chartName := p[1]
return u, fmt.Errorf("no URL found for repository %q", p[0]) rf, err := findRepoEntry(repoName, r.Repositories)
} if err != nil {
baseURL := rf.URL return u, err
if !strings.HasSuffix(baseURL, "/") { }
baseURL = baseURL + "/" if rf.URL == "" {
} return u, fmt.Errorf("no URL found for repository %q", repoName)
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/")) }
// Next, we need to load the index, and actually look up the chart.
i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(repoName))
if err != nil {
return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
}
cv, err := i.Get(chartName, version)
if err != nil {
return u, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, repoName, err)
}
if len(cv.URLs) == 0 {
return u, fmt.Errorf("chart %q has no downloadable URLs", ref)
}
return url.Parse(cv.URLs[0])
}
// urlJoin joins a base URL to one or more path components.
//
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
// perform a join.
//
// If the URL is unparsable, this returns an error.
func urlJoin(baseURL string, paths ...string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
} }
return u, fmt.Errorf("invalid chart url format: %s", ref) // We want path instead of filepath because path always uses /.
all := []string{u.Path}
all = append(all, paths...)
u.Path = path.Join(all...)
return u.String(), nil
} }
func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) {

@ -30,12 +30,14 @@ import (
func TestResolveChartRef(t *testing.T) { func TestResolveChartRef(t *testing.T) {
tests := []struct { tests := []struct {
name, ref, expect string name, ref, expect, version string
fail bool fail bool
}{ }{
{name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, {name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"},
{name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"}, {name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"},
{name: "reference, testing repo", ref: "testing/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, {name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz"},
{name: "reference, testing repo", ref: "testing/alpine", expect: "http://example.com/alpine-1.2.3.tgz"},
{name: "reference, version, testing repo", ref: "testing/alpine", version: "0.2.0", expect: "http://example.com/alpine-0.2.0.tgz"},
{name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true},
{name: "invalid", ref: "invalid-1.2.3", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true},
{name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true},
@ -47,7 +49,7 @@ func TestResolveChartRef(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
u, err := c.ResolveChartVersion(tt.ref) u, err := c.ResolveChartVersion(tt.ref, tt.version)
if err != nil { if err != nil {
if tt.fail { if tt.fail {
continue continue
@ -132,12 +134,16 @@ func TestDownloadTo(t *testing.T) {
Keyring: "testdata/helm-test-key.pub", Keyring: "testdata/helm-test-key.pub",
} }
cname := "/signtest-0.1.0.tgz" cname := "/signtest-0.1.0.tgz"
v, err := c.DownloadTo(srv.URL()+cname, dest) where, v, err := c.DownloadTo(srv.URL()+cname, "", dest)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
if expect := filepath.Join(dest, cname); where != expect {
t.Errorf("Expected download to %s, got %s", expect, where)
}
if v.FileHash == "" { if v.FileHash == "" {
t.Error("File hash was empty, but verification is required.") t.Error("File hash was empty, but verification is required.")
} }
@ -147,3 +153,24 @@ func TestDownloadTo(t *testing.T) {
return return
} }
} }
func TestUrlJoin(t *testing.T) {
tests := []struct {
name, url, expect string
paths []string
}{
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
}
for _, tt := range tests {
if got, err := urlJoin(tt.url, tt.paths...); err != nil {
t.Errorf("%s: error %q", tt.name, err)
} else if got != tt.expect {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
}
}
}

@ -184,7 +184,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
} }
dest := filepath.Join(m.ChartPath, "charts") dest := filepath.Join(m.ChartPath, "charts")
if _, err := dl.DownloadTo(churl, dest); err != nil { if _, _, err := dl.DownloadTo(churl, "", dest); err != nil {
fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err) fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err)
continue continue
} }
@ -270,36 +270,61 @@ func urlsAreEqual(a, b string) bool {
return au.String() == bu.String() return au.String() == bu.String()
} }
// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified. // findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified.
// //
// 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the // 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the
// newest version will be returned. // newest version will be returned.
// //
// repourl is the repository to search // repoURL is the repository to search
// //
// If it finds a URL that is "relative", it will prepend the repourl. // If it finds a URL that is "relative", it will prepend the repoURL.
func findChartURL(name, version, repourl string, repos map[string]*repo.ChartRepository) (string, error) { func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos { for _, cr := range repos {
if urlsAreEqual(repourl, cr.URL) { if urlsAreEqual(repoURL, cr.URL) {
for ename, entry := range cr.IndexFile.Entries { entry, err := findEntryByName(name, cr)
if ename == name { if err != nil {
for _, verEntry := range entry { return "", err
if len(verEntry.URLs) == 0 { }
// Not a legit entry. ve, err := findVersionedEntry(version, entry)
continue if err != nil {
} return "", err
if version == "" || versionEquals(version, verEntry.Version) {
return normalizeURL(repourl, verEntry.URLs[0])
}
return normalizeURL(repourl, verEntry.URLs[0])
}
}
} }
return normalizeURL(repoURL, ve.URLs[0])
} }
} }
return "", fmt.Errorf("chart %s not found in %s", name, repourl) return "", fmt.Errorf("chart %s not found in %s", name, repoURL)
}
// findEntryByName finds an entry in the chart repository whose name matches the given name.
//
// It returns the ChartVersions for that entry.
func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) {
for ename, entry := range cr.IndexFile.Entries {
if ename == name {
return entry, nil
}
}
return nil, errors.New("entry not found")
}
// findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints.
//
// If version is empty, the first chart found is returned.
func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) {
for _, verEntry := range vers {
if len(verEntry.URLs) == 0 {
// Not a legit entry.
continue
}
if version == "" || versionEquals(version, verEntry.Version) {
return verEntry, nil
}
return verEntry, nil
}
return nil, errors.New("no matching version")
} }
func versionEquals(v1, v2 string) bool { func versionEquals(v1, v2 string) bool {

@ -20,7 +20,6 @@ import (
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
//"k8s.io/helm/pkg/repo"
) )
func TestVersionEquals(t *testing.T) { func TestVersionEquals(t *testing.T) {

@ -1 +1,30 @@
apiVersion: v1 apiVersion: v1
entries:
alpine:
- name: alpine
urls:
- http://example.com/alpine-1.2.3.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 1.2.3
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
- name: alpine
urls:
- http://example.com/alpine-0.2.0.tgz
- 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: ""

@ -49,6 +49,7 @@ type fetchCmd struct {
untardir string untardir string
chartRef string chartRef string
destdir string destdir string
version string
verify bool verify bool
keyring string keyring string
@ -81,6 +82,7 @@ func newFetchCmd(out io.Writer) *cobra.Command {
f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.") f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.")
f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies the name of the directory into which the chart is expanded.") f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies the name of the directory into which the chart is expanded.")
f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.") f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.")
f.StringVar(&fch.version, "version", "", "The specific version of a chart. Without this, the latest version is fetched.")
f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
f.StringVarP(&fch.destdir, "destination", "d", ".", "The location to write the chart. If this and tardir are specified, tardir is appended to this.") f.StringVarP(&fch.destdir, "destination", "d", ".", "The location to write the chart. If this and tardir are specified, tardir is appended to this.")
@ -89,10 +91,6 @@ func newFetchCmd(out io.Writer) *cobra.Command {
func (f *fetchCmd) run() error { func (f *fetchCmd) run() error {
pname := f.chartRef pname := f.chartRef
if filepath.Ext(pname) != ".tgz" {
pname += ".tgz"
}
c := downloader.ChartDownloader{ c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()), HelmHome: helmpath.Home(homePath()),
Out: f.out, Out: f.out,
@ -116,7 +114,7 @@ func (f *fetchCmd) run() error {
defer os.RemoveAll(dest) defer os.RemoveAll(dest)
} }
v, err := c.DownloadTo(pname, dest) saved, v, err := c.DownloadTo(pname, f.version, dest)
if err != nil { if err != nil {
return err return err
} }
@ -140,8 +138,7 @@ func (f *fetchCmd) run() error {
return fmt.Errorf("Failed to untar: %s is not a directory", ud) return fmt.Errorf("Failed to untar: %s is not a directory", ud)
} }
from := filepath.Join(dest, filepath.Base(pname)) return chartutil.ExpandFile(ud, saved)
return chartutil.ExpandFile(ud, from)
} }
return nil return nil
} }

@ -49,38 +49,51 @@ func TestFetchCmd(t *testing.T) {
}{ }{
{ {
name: "Basic chart fetch", name: "Basic chart fetch",
chart: "test/signtest-0.1.0", chart: "test/signtest",
expectFile: "./signtest-0.1.0.tgz", expectFile: "./signtest-0.1.0.tgz",
}, },
{
name: "Chart fetch with version",
chart: "test/signtest",
flags: []string{"--version", "0.1.0"},
expectFile: "./signtest-0.1.0.tgz",
},
{
name: "Fail chart fetch with non-existent version",
chart: "test/signtest",
flags: []string{"--version", "99.1.0"},
fail: true,
failExpect: "no such chart",
},
{ {
name: "Fail fetching non-existent chart", name: "Fail fetching non-existent chart",
chart: "test/nosuchthing-0.1.0", chart: "test/nosuchthing",
failExpect: "Failed to fetch", failExpect: "Failed to fetch",
fail: true, fail: true,
}, },
{ {
name: "Fetch and verify", name: "Fetch and verify",
chart: "test/signtest-0.1.0", chart: "test/signtest",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"},
expectFile: "./signtest-0.1.0.tgz", expectFile: "./signtest-0.1.0.tgz",
}, },
{ {
name: "Fetch and fail verify", name: "Fetch and fail verify",
chart: "test/reqtest-0.1.0", chart: "test/reqtest",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"},
failExpect: "Failed to fetch provenance", failExpect: "Failed to fetch provenance",
fail: true, fail: true,
}, },
{ {
name: "Fetch and untar", name: "Fetch and untar",
chart: "test/signtest-0.1.0", chart: "test/signtest",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"},
expectFile: "./signtest", expectFile: "./signtest",
expectDir: true, expectDir: true,
}, },
{ {
name: "Fetch, verify, untar", name: "Fetch, verify, untar",
chart: "test/signtest-0.1.0", chart: "test/signtest",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"},
expectFile: "./signtest", expectFile: "./signtest",
expectDir: true, expectDir: true,
@ -93,8 +106,9 @@ func TestFetchCmd(t *testing.T) {
if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := srv.LinkIndices(); err != nil {
t.Logf("HELM_HOME=%s", homePath()) t.Fatal(err)
}
for _, tt := range tests { for _, tt := range tests {
outdir := filepath.Join(hh, "testout") outdir := filepath.Join(hh, "testout")

@ -28,7 +28,8 @@ import (
) )
const inspectDesc = ` const inspectDesc = `
This command inspects a chart (directory, file, or URL) and displays information. This command inspects a chart and displays information. It takes a chart reference
('stable/drupal'), a full path to a directory or packaged chart, or a URL.
Inspect prints the contents of the Chart.yaml file and the values.yaml file. Inspect prints the contents of the Chart.yaml file and the values.yaml file.
` `
@ -50,6 +51,7 @@ type inspectCmd struct {
keyring string keyring string
out io.Writer out io.Writer
client helm.Interface client helm.Interface
version string
} }
const ( const (
@ -73,7 +75,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
if err := checkArgsLength(len(args), "chart name"); err != nil { if err := checkArgsLength(len(args), "chart name"); err != nil {
return err return err
} }
cp, err := locateChartPath(args[0], insp.verify, insp.keyring) cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring)
if err != nil { if err != nil {
return err return err
} }
@ -88,7 +90,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: inspectValuesDesc, Long: inspectValuesDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
insp.output = valuesOnly insp.output = valuesOnly
cp, err := locateChartPath(args[0], insp.verify, insp.keyring) cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring)
if err != nil { if err != nil {
return err return err
} }
@ -103,7 +105,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
Long: inspectChartDesc, Long: inspectChartDesc,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
insp.output = chartOnly insp.output = chartOnly
cp, err := locateChartPath(args[0], insp.verify, insp.keyring) cp, err := locateChartPath(args[0], insp.version, insp.verify, insp.keyring)
if err != nil { if err != nil {
return err return err
} }
@ -125,6 +127,12 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command {
valuesSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) valuesSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc)
chartSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) chartSubCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc)
verflag := "version"
verdesc := "the version of the chart. By default, the newest chart is shown."
inspectCommand.Flags().StringVar(&insp.version, verflag, "", verdesc)
valuesSubCmd.Flags().StringVar(&insp.version, verflag, "", verdesc)
chartSubCmd.Flags().StringVar(&insp.version, verflag, "", verdesc)
inspectCommand.AddCommand(valuesSubCmd) inspectCommand.AddCommand(valuesSubCmd)
inspectCommand.AddCommand(chartSubCmd) inspectCommand.AddCommand(chartSubCmd)

@ -48,11 +48,11 @@ name of a chart in the current working directory.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use either the '--values' flag and pass in a file
or use the '--set' flag and pass configuration from the command line. or use the '--set' flag and pass configuration from the command line.
$ helm install -f myvalues.yaml redis $ helm install -f myvalues.yaml ./redis
or or
$ helm install --set name=prod redis $ helm install --set name=prod ./redis
To check the generated manifests of a release without installing the chart, To check the generated manifests of a release without installing the chart,
the '--debug' and '--dry-run' flags can be combined. This will still require a the '--debug' and '--dry-run' flags can be combined. This will still require a
@ -60,6 +60,26 @@ round-trip to the Tiller server.
If --verify is set, the chart MUST have a provenance file, and the provenenace If --verify is set, the chart MUST have a provenance file, and the provenenace
fall MUST pass all verification steps. fall MUST pass all verification steps.
There are four different ways you can express the chart you want to install:
1. By chart reference: helm install stable/mariadb
2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz
3. By path to an unpacked chart directory: helm install ./nginx
4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz
CHART REFERENCES
A chart reference is a convenient way of reference a chart in a chart repository.
When you use a chart reference ('stable/mariadb'), Helm will look in the local
configuration for a chart repository named 'stable', and will then look for a
chart in that repository whose name is 'mariadb'. It will install the latest
version of that chart unless you also supply a version number with the
'--version' flag.
To see the list of chart repositories, use 'helm repo list'. To search for
charts in a repository, use 'helm search'.
` `
type installCmd struct { type installCmd struct {
@ -76,6 +96,7 @@ type installCmd struct {
client helm.Interface client helm.Interface
values *values values *values
nameTemplate string nameTemplate string
version string
} }
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
@ -94,7 +115,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
if err := checkArgsLength(len(args), "chart name"); err != nil { if err := checkArgsLength(len(args), "chart name"); err != nil {
return err return err
} }
cp, err := locateChartPath(args[0], inst.verify, inst.keyring) cp, err := locateChartPath(args[0], inst.version, inst.verify, inst.keyring)
if err != nil { if err != nil {
return err return err
} }
@ -116,6 +137,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release")
f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it")
f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed.")
return cmd return cmd
} }
@ -276,9 +298,12 @@ func splitPair(item string) (name string, value interface{}) {
// - current working directory // - current working directory
// - if path is absolute or begins with '.', error out here // - if path is absolute or begins with '.', error out here
// - chart repos in $HELM_HOME // - chart repos in $HELM_HOME
// - URL
// //
// If 'verify' is true, this will attempt to also verify the chart. // If 'verify' is true, this will attempt to also verify the chart.
func locateChartPath(name string, verify bool, keyring string) (string, error) { func locateChartPath(name, version string, verify bool, keyring string) (string, error) {
name = strings.TrimSpace(name)
version = strings.TrimSpace(version)
if fi, err := os.Stat(name); err == nil { if fi, err := os.Stat(name); err == nil {
abs, err := filepath.Abs(name) abs, err := filepath.Abs(name)
if err != nil { if err != nil {
@ -303,12 +328,6 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
return filepath.Abs(crepo) return filepath.Abs(crepo)
} }
// Try fetching the chart from a remote repo into a tmpdir
origname := name
if filepath.Ext(name) != ".tgz" {
name += ".tgz"
}
dl := downloader.ChartDownloader{ dl := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()), HelmHome: helmpath.Home(homePath()),
Out: os.Stdout, Out: os.Stdout,
@ -318,16 +337,19 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
dl.Verify = downloader.VerifyAlways dl.Verify = downloader.VerifyAlways
} }
if _, err := dl.DownloadTo(name, "."); err == nil { filename, _, err := dl.DownloadTo(name, version, ".")
lname, err := filepath.Abs(filepath.Base(name)) if err == nil {
lname, err := filepath.Abs(filename)
if err != nil { if err != nil {
return lname, err return filename, err
} }
fmt.Printf("Fetched %s to %s\n", origname, lname) fmt.Printf("Fetched %s to %s\n", name, filename)
return lname, nil return lname, nil
} else if flagDebug {
return filename, err
} }
return name, fmt.Errorf("file %q not found", origname) return filename, fmt.Errorf("file %q not found", name)
} }
func generateName(nameTemplate string) (string, error) { func generateName(nameTemplate string) (string, error) {

@ -97,6 +97,9 @@ func (s *searchCmd) showAllCharts(i *search.Index) {
} }
func (s *searchCmd) formatSearchResults(res []*search.Result) string { func (s *searchCmd) formatSearchResults(res []*search.Result) string {
if len(res) == 0 {
return "No results found"
}
table := uitable.New() table := uitable.New()
table.MaxColWidth = 50 table.MaxColWidth = 50
table.AddRow("NAME", "VERSION", "DESCRIPTION") table.AddRow("NAME", "VERSION", "DESCRIPTION")
@ -119,7 +122,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) {
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 or missing. Try 'helm repo update':\n\t%s\n", f, err) fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
continue continue
} }

@ -50,7 +50,7 @@ func TestSearchCmd(t *testing.T) {
{ {
name: "search for 'syzygy', expect no matches", name: "search for 'syzygy', expect no matches",
args: []string{"syzygy"}, args: []string{"syzygy"},
expect: "NAME\tVERSION\tDESCRIPTION", expect: "No results found",
}, },
{ {
name: "search for 'alp[a-z]+', expect two matches", name: "search for 'alp[a-z]+', expect two matches",

@ -33,7 +33,9 @@ const upgradeDesc = `
This command upgrades a release to a new version of a chart. This command upgrades a release to a new version of a chart.
The upgrade arguments must be a release and a chart. The chart The upgrade arguments must be a release and a chart. The chart
argument can be a relative path to a packaged or unpackaged chart. argument can a chart reference ('stable/mariadb'), a path to a chart directory
or packaged chart, or a fully qualified URL. For chart references, the latest
version will be specified unless the '--version' flag is set.
To override values in a chart, use either the '--values' flag and pass in a file To override values in a chart, use either the '--values' flag and pass in a file
or use the '--set' flag and pass configuration from the command line. or use the '--set' flag and pass configuration from the command line.
@ -52,6 +54,7 @@ type upgradeCmd struct {
keyring string keyring string
install bool install bool
namespace string namespace string
version string
} }
func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
@ -89,12 +92,13 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys") f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "the path to the keyring that contains public singing keys")
f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.StringVar(&upgrade.namespace, "namespace", "default", "the namespace to install the release into (only used if --install is set)") f.StringVar(&upgrade.namespace, "namespace", "default", "the namespace to install the release into (only used if --install is set)")
f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used.")
return cmd return cmd
} }
func (u *upgradeCmd) run() error { func (u *upgradeCmd) run() error {
chartPath, err := locateChartPath(u.chart, u.verify, u.keyring) chartPath, err := locateChartPath(u.chart, u.version, u.verify, u.keyring)
if err != nil { if err != nil {
return err return err
} }

@ -7,6 +7,11 @@ This guide covers how you can quickly get started using Helm.
- You must have Kubernetes installed, and have a local configured copy - You must have Kubernetes installed, and have a local configured copy
of `kubectl`. of `kubectl`.
Helm will figure out where to install Tiller by reading your Kubernetes
configuration file (usually `$HOME/.kube/config`). This is the same file
that `kubectl` uses, so to find out which cluster Tiller would install
to, you can run `kubectl cluster-info`.
## Install Helm ## Install Helm
Download a binary release of the Helm client from Download a binary release of the Helm client from
@ -27,20 +32,19 @@ $ helm init
## Install an Example Chart ## Install an Example Chart
To install a chart, you can run the `helm install` command. To install a chart, you can run the `helm install` command.
Let's use an example chart from this repository. Let's use an example chart from this repository.
Make sure you are in the root directory of this repo. Make sure you are in the root directory of this repo.
```console ```console
$ helm install docs/examples/alpine $ helm install stable/mysql
Released smiling-penguin Released smiling-penguin
``` ```
In the example above, the `alpine` chart was released, and the name of In the example above, the `stable/mysql` chart was released, and the name of
our new release is `smiling-penguin`. You can view the details of the chart we just our new release is `smiling-penguin`. You get a simple idea of this
installed by taking a look at the nginx chart in MySQL chart by running `helm inspect stable/mysql`.
[docs/examples/alpine/Chart.yaml](examples/alpine/Chart.yaml).
## Change a Default Chart Value ## Change a Default Chart Value
@ -48,7 +52,7 @@ A nice feature of helm is the ability to change certain values of the package fo
Let's install the `nginx` example from this repository but change the `replicaCount` to 7. Let's install the `nginx` example from this repository but change the `replicaCount` to 7.
```console ```console
$ helm install --set replicaCount=7 docs/examples/nginx $ helm install --set replicaCount=7 ./docs/examples/nginx
happy-panda happy-panda
``` ```
@ -65,6 +69,9 @@ $ helm status smiling-penguin
Status: DEPLOYED Status: DEPLOYED
``` ```
The `status` command will display information about a release in your
cluster.
## Uninstall a Release ## Uninstall a Release
To uninstall a release, use the `helm delete` command: To uninstall a release, use the `helm delete` command:

@ -181,7 +181,7 @@ To see what options are configurable on a chart, use `helm inspect
values`: values`:
```console ```console
helm inspect values stable/mariadb-0.3.0.tgz helm inspect values stable/mariadb
Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz
## Bitnami MariaDB image version ## Bitnami MariaDB image version
## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ ## ref: https://hub.docker.com/r/bitnami/mariadb/tags/
@ -235,7 +235,7 @@ complex, Helm tries to perform the least invasive upgrade. It will only
update things that have changed since the last release. update things that have changed since the last release.
```console ```console
$ helm upgrade -f panda.yaml happy-panda stable/mariadb-0.3.0.tgz 1 ↵ $ helm upgrade -f panda.yaml happy-panda stable/mariadb
Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz
happy-panda has been upgraded. Happy Helming! happy-panda has been upgraded. Happy Helming!
Last Deployed: Wed Sep 28 12:47:54 2016 Last Deployed: Wed Sep 28 12:47:54 2016

@ -60,6 +60,8 @@ type Verification struct {
SignedBy *openpgp.Entity SignedBy *openpgp.Entity
// FileHash is the hash, prepended with the scheme, for the file that was verified. // FileHash is the hash, prepended with the scheme, for the file that was verified.
FileHash string FileHash string
// FileName is the name of the file that FileHash verifies.
FileName string
} }
// Signatory signs things. // Signatory signs things.
@ -221,6 +223,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum)
} }
ver.FileHash = sum ver.FileHash = sum
ver.FileName = basename
// TODO: when image signing is added, verify that here. // TODO: when image signing is added, verify that here.

@ -18,6 +18,7 @@ package provenance
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings" "strings"
"testing" "testing"
@ -246,6 +247,8 @@ func TestVerify(t *testing.T) {
t.Error("Verification is missing hash.") t.Error("Verification is missing hash.")
} else if ver.SignedBy == nil { } else if ver.SignedBy == nil {
t.Error("No SignedBy field") t.Error("No SignedBy field")
} else if ver.FileName != filepath.Base(testChartfile) {
t.Errorf("FileName is unexpectedly %q", ver.FileName)
} }
if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil { if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil {

@ -138,7 +138,10 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
if !ok { if !ok {
return nil, ErrNoChartName return nil, ErrNoChartName
} }
if version == "" && len(vs) > 0 { if len(vs) == 0 {
return nil, ErrNoChartVersion
}
if len(version) == 0 {
return vs[0], nil return vs[0], nil
} }
for _, ver := range vs { for _, ver := range vs {
@ -147,7 +150,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
return ver, nil return ver, nil
} }
} }
return nil, ErrNoChartVersion return nil, fmt.Errorf("No chart version found for %s-%s", name, version)
} }
// WriteFile writes an index file to the given destination path. // WriteFile writes an index file to the given destination path.

@ -123,8 +123,6 @@ func (s *Server) CreateIndex() error {
return err return err
} }
println(string(d))
ifile := filepath.Join(s.docroot, "index.yaml") ifile := filepath.Join(s.docroot, "index.yaml")
return ioutil.WriteFile(ifile, d, 0755) return ioutil.WriteFile(ifile, d, 0755)
} }
@ -148,11 +146,23 @@ func (s *Server) URL() string {
return s.srv.URL return s.srv.URL
} }
// LinkIndices links the index created with CreateIndex and makes a symboic link to the repositories/cache directory.
//
// This makes it possible to simulate a local cache of a repository.
func (s *Server) LinkIndices() error {
destfile := "test-index.yaml"
// Link the index.yaml file to the
lstart := filepath.Join(s.docroot, "index.yaml")
ldest := filepath.Join(s.docroot, "repository/cache", destfile)
return os.Symlink(lstart, ldest)
}
// 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 {
rf := repo.NewRepoFile() rf := repo.NewRepoFile()
rf.Add(&repo.Entry{Name: name, URL: url}) rf.Add(&repo.Entry{Name: name, URL: url})
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 rf.WriteFile(dest, 0644) return rf.WriteFile(dest, 0644)
} }

Loading…
Cancel
Save