From 1c6fc9c0e857c1876f310bbbd6678134a00566ef Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 5 Oct 2016 12:43:06 -0600 Subject: [PATCH] feat(helm): remove the requirement that fetch/install need version This removes the requirement that a fetch or install command must explicitly state the version number to install. Instead, this goes to the strategy used by OS package managers: Install the latest until told to do otherwise. Closes #1198 --- README.md | 2 + cmd/helm/downloader/chart_downloader.go | 97 ++++++++++++++----- cmd/helm/downloader/chart_downloader_test.go | 37 ++++++- cmd/helm/downloader/manager.go | 69 ++++++++----- cmd/helm/downloader/manager_test.go | 1 - .../repository/cache/testing-index.yaml | 29 ++++++ cmd/helm/fetch.go | 11 +-- cmd/helm/fetch_test.go | 30 ++++-- cmd/helm/inspect.go | 16 ++- cmd/helm/install.go | 52 +++++++--- cmd/helm/search.go | 5 +- cmd/helm/search_test.go | 2 +- cmd/helm/upgrade.go | 8 +- docs/quickstart.md | 23 +++-- docs/using_helm.md | 4 +- pkg/provenance/sign.go | 3 + pkg/provenance/sign_test.go | 3 + pkg/repo/index.go | 7 +- pkg/repo/repotest/server.go | 14 ++- 19 files changed, 307 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 6c624d098..b9589dcbc 100644 --- a/README.md +++ b/README.md @@ -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`. +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, including installing pre-releases. diff --git a/cmd/helm/downloader/chart_downloader.go b/cmd/helm/downloader/chart_downloader.go index db8db771d..2871f1dea 100644 --- a/cmd/helm/downloader/chart_downloader.go +++ b/cmd/helm/downloader/chart_downloader.go @@ -24,6 +24,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "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. // // 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 - u, err := c.ResolveChartVersion(ref) + u, err := c.ResolveChartVersion(ref, version) if err != nil { - return nil, err + return "", nil, err } data, err := download(u.String()) if err != nil { - return nil, err + return "", nil, err } name := filepath.Base(u.Path) destfile := filepath.Join(dest, name) if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil { - return nil, err + return destfile, nil, err } // 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") if err != nil { 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) - return ver, nil + return destfile, ver, nil } provfile := destfile + ".prov" if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil { - return nil, err + return destfile, nil, err } ver, err = VerifyChart(destfile, c.Keyring) if err != nil { // Fail always in this case, since it means the verification step // failed. - return ver, err + return destfile, ver, err } } - return ver, nil + return destfile, ver, nil } // 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) 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. + // FIXME: Why do we use url.ParseRequestURI instead of url.Parse? u, err := url.ParseRequestURI(ref) if err == nil { // 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 - p := strings.Split(ref, "/") - if len(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:], "/")) + p := strings.SplitN(ref, "/", 2) + if len(p) < 2 { + return u, fmt.Errorf("invalid chart url format: %s", ref) + } + + repoName := p[0] + chartName := p[1] + rf, err := findRepoEntry(repoName, r.Repositories) + if err != nil { + return u, err + } + if rf.URL == "" { + return u, fmt.Errorf("no URL found for repository %q", repoName) + } + + // 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) { diff --git a/cmd/helm/downloader/chart_downloader_test.go b/cmd/helm/downloader/chart_downloader_test.go index c8bbfa8e6..545943304 100644 --- a/cmd/helm/downloader/chart_downloader_test.go +++ b/cmd/helm/downloader/chart_downloader_test.go @@ -30,12 +30,14 @@ import ( func TestResolveChartRef(t *testing.T) { tests := []struct { - name, ref, expect string - fail bool + name, ref, expect, version string + 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, 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: "invalid", ref: "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 { - u, err := c.ResolveChartVersion(tt.ref) + u, err := c.ResolveChartVersion(tt.ref, tt.version) if err != nil { if tt.fail { continue @@ -132,12 +134,16 @@ func TestDownloadTo(t *testing.T) { Keyring: "testdata/helm-test-key.pub", } 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 { t.Error(err) return } + if expect := filepath.Join(dest, cname); where != expect { + t.Errorf("Expected download to %s, got %s", expect, where) + } + if v.FileHash == "" { t.Error("File hash was empty, but verification is required.") } @@ -147,3 +153,24 @@ func TestDownloadTo(t *testing.T) { 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) + } + } +} diff --git a/cmd/helm/downloader/manager.go b/cmd/helm/downloader/manager.go index 373714cee..c140504fc 100644 --- a/cmd/helm/downloader/manager.go +++ b/cmd/helm/downloader/manager.go @@ -184,7 +184,7 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { } 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) continue } @@ -270,36 +270,61 @@ func urlsAreEqual(a, b string) bool { 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 // 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. -func findChartURL(name, version, repourl string, repos map[string]*repo.ChartRepository) (string, error) { +// If it finds a URL that is "relative", it will prepend the repoURL. +func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) { for _, cr := range repos { - if urlsAreEqual(repourl, cr.URL) { - for ename, entry := range cr.IndexFile.Entries { - if ename == name { - for _, verEntry := range entry { - if len(verEntry.URLs) == 0 { - // Not a legit entry. - continue - } - - if version == "" || versionEquals(version, verEntry.Version) { - return normalizeURL(repourl, verEntry.URLs[0]) - } - - return normalizeURL(repourl, verEntry.URLs[0]) - } - } + if urlsAreEqual(repoURL, cr.URL) { + entry, err := findEntryByName(name, cr) + if err != nil { + return "", err + } + ve, err := findVersionedEntry(version, entry) + if err != nil { + return "", err } + + 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 { diff --git a/cmd/helm/downloader/manager_test.go b/cmd/helm/downloader/manager_test.go index 3910d1684..b8b32e7d6 100644 --- a/cmd/helm/downloader/manager_test.go +++ b/cmd/helm/downloader/manager_test.go @@ -20,7 +20,6 @@ import ( "testing" "k8s.io/helm/cmd/helm/helmpath" - //"k8s.io/helm/pkg/repo" ) func TestVersionEquals(t *testing.T) { diff --git a/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml index 9cde8e8dd..4a46c7b8b 100644 --- a/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml +++ b/cmd/helm/downloader/testdata/helmhome/repository/cache/testing-index.yaml @@ -1 +1,30 @@ 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: "" diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go index 5491f5299..84acea792 100644 --- a/cmd/helm/fetch.go +++ b/cmd/helm/fetch.go @@ -49,6 +49,7 @@ type fetchCmd struct { untardir string chartRef string destdir string + version string verify bool 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.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.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.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 { pname := f.chartRef - if filepath.Ext(pname) != ".tgz" { - pname += ".tgz" - } - c := downloader.ChartDownloader{ HelmHome: helmpath.Home(homePath()), Out: f.out, @@ -116,7 +114,7 @@ func (f *fetchCmd) run() error { defer os.RemoveAll(dest) } - v, err := c.DownloadTo(pname, dest) + saved, v, err := c.DownloadTo(pname, f.version, dest) if err != nil { return err } @@ -140,8 +138,7 @@ func (f *fetchCmd) run() error { return fmt.Errorf("Failed to untar: %s is not a directory", ud) } - from := filepath.Join(dest, filepath.Base(pname)) - return chartutil.ExpandFile(ud, from) + return chartutil.ExpandFile(ud, saved) } return nil } diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go index b24d255e7..286537a7d 100644 --- a/cmd/helm/fetch_test.go +++ b/cmd/helm/fetch_test.go @@ -49,38 +49,51 @@ func TestFetchCmd(t *testing.T) { }{ { name: "Basic chart fetch", - chart: "test/signtest-0.1.0", + chart: "test/signtest", 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", - chart: "test/nosuchthing-0.1.0", + chart: "test/nosuchthing", failExpect: "Failed to fetch", fail: true, }, { name: "Fetch and verify", - chart: "test/signtest-0.1.0", + chart: "test/signtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, expectFile: "./signtest-0.1.0.tgz", }, { name: "Fetch and fail verify", - chart: "test/reqtest-0.1.0", + chart: "test/reqtest", flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, failExpect: "Failed to fetch provenance", fail: true, }, { 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"}, expectFile: "./signtest", expectDir: true, }, { 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"}, expectFile: "./signtest", expectDir: true, @@ -93,8 +106,9 @@ func TestFetchCmd(t *testing.T) { if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { t.Fatal(err) } - - t.Logf("HELM_HOME=%s", homePath()) + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } for _, tt := range tests { outdir := filepath.Join(hh, "testout") diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index 8386fd1d0..dd15b7dc2 100644 --- a/cmd/helm/inspect.go +++ b/cmd/helm/inspect.go @@ -28,7 +28,8 @@ import ( ) 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. ` @@ -50,6 +51,7 @@ type inspectCmd struct { keyring string out io.Writer client helm.Interface + version string } const ( @@ -73,7 +75,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { if err := checkArgsLength(len(args), "chart name"); err != nil { 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 { return err } @@ -88,7 +90,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectValuesDesc, RunE: func(cmd *cobra.Command, args []string) error { 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 { return err } @@ -103,7 +105,7 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { Long: inspectChartDesc, RunE: func(cmd *cobra.Command, args []string) error { 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 { return err } @@ -125,6 +127,12 @@ func newInspectCmd(c helm.Interface, out io.Writer) *cobra.Command { valuesSubCmd.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(chartSubCmd) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 648c2b5ea..d53dd7bd2 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -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 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 - $ helm install --set name=prod redis + $ helm install --set name=prod ./redis 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 @@ -60,6 +60,26 @@ round-trip to the Tiller server. If --verify is set, the chart MUST have a provenance file, and the provenenace 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 { @@ -76,6 +96,7 @@ type installCmd struct { client helm.Interface values *values nameTemplate string + version string } 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 { 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 { 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.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.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed.") return cmd } @@ -276,9 +298,12 @@ func splitPair(item string) (name string, value interface{}) { // - current working directory // - if path is absolute or begins with '.', error out here // - chart repos in $HELM_HOME +// - URL // // 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 { abs, err := filepath.Abs(name) if err != nil { @@ -303,12 +328,6 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { 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{ HelmHome: helmpath.Home(homePath()), Out: os.Stdout, @@ -318,16 +337,19 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { dl.Verify = downloader.VerifyAlways } - if _, err := dl.DownloadTo(name, "."); err == nil { - lname, err := filepath.Abs(filepath.Base(name)) + filename, _, err := dl.DownloadTo(name, version, ".") + if err == nil { + lname, err := filepath.Abs(filename) 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 + } 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) { diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 020b6fe5f..6028aa6c1 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -97,6 +97,9 @@ func (s *searchCmd) showAllCharts(i *search.Index) { } func (s *searchCmd) formatSearchResults(res []*search.Result) string { + if len(res) == 0 { + return "No results found" + } table := uitable.New() table.MaxColWidth = 50 table.AddRow("NAME", "VERSION", "DESCRIPTION") @@ -119,7 +122,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) { f := s.helmhome.CacheIndex(n) ind, err := repo.LoadIndexFile(f) 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 } diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 999e79414..b81a3536d 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -50,7 +50,7 @@ func TestSearchCmd(t *testing.T) { { name: "search for 'syzygy', expect no matches", args: []string{"syzygy"}, - expect: "NAME\tVERSION\tDESCRIPTION", + expect: "No results found", }, { name: "search for 'alp[a-z]+', expect two matches", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index b91efbbc6..d85bf8cfe 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -33,7 +33,9 @@ const upgradeDesc = ` This command upgrades a release to a new version of a 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 or use the '--set' flag and pass configuration from the command line. @@ -52,6 +54,7 @@ type upgradeCmd struct { keyring string install bool namespace string + version string } 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.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.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used.") return cmd } 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 { return err } diff --git a/docs/quickstart.md b/docs/quickstart.md index b9ecf846a..3266f9151 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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 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 Download a binary release of the Helm client from @@ -27,20 +32,19 @@ $ helm init ## Install an Example Chart -To install a chart, you can run the `helm install` command. -Let's use an example chart from this repository. +To install a chart, you can run the `helm install` command. +Let's use an example chart from this repository. Make sure you are in the root directory of this repo. ```console -$ helm install docs/examples/alpine +$ helm install stable/mysql Released smiling-penguin ``` -In the example above, the `alpine` chart was released, and the name of -our new release is `smiling-penguin`. You can view the details of the chart we just -installed by taking a look at the nginx chart in -[docs/examples/alpine/Chart.yaml](examples/alpine/Chart.yaml). +In the example above, the `stable/mysql` chart was released, and the name of +our new release is `smiling-penguin`. You get a simple idea of this +MySQL chart by running `helm inspect stable/mysql`. ## 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. ```console -$ helm install --set replicaCount=7 docs/examples/nginx +$ helm install --set replicaCount=7 ./docs/examples/nginx happy-panda ``` @@ -65,6 +69,9 @@ $ helm status smiling-penguin Status: DEPLOYED ``` +The `status` command will display information about a release in your +cluster. + ## Uninstall a Release To uninstall a release, use the `helm delete` command: diff --git a/docs/using_helm.md b/docs/using_helm.md index 155da24e5..5665bc5e4 100644 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -181,7 +181,7 @@ To see what options are configurable on a chart, use `helm inspect values`: ```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 ## Bitnami MariaDB image version ## 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. ```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 happy-panda has been upgraded. Happy Helming! Last Deployed: Wed Sep 28 12:47:54 2016 diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index ae4c6d2b6..90b14cdc5 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -60,6 +60,8 @@ type Verification struct { SignedBy *openpgp.Entity // FileHash is the hash, prepended with the scheme, for the file that was verified. FileHash string + // FileName is the name of the file that FileHash verifies. + FileName string } // 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) } ver.FileHash = sum + ver.FileName = basename // TODO: when image signing is added, verify that here. diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 00bc5aced..747a9376a 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -18,6 +18,7 @@ package provenance import ( "io/ioutil" "os" + "path/filepath" "strings" "testing" @@ -246,6 +247,8 @@ func TestVerify(t *testing.T) { t.Error("Verification is missing hash.") } else if ver.SignedBy == nil { 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 { diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 9fe08b7e0..5b591f7e5 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -138,7 +138,10 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { if !ok { return nil, ErrNoChartName } - if version == "" && len(vs) > 0 { + if len(vs) == 0 { + return nil, ErrNoChartVersion + } + if len(version) == 0 { return vs[0], nil } for _, ver := range vs { @@ -147,7 +150,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { 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. diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 9223ed0a3..8094a245c 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -123,8 +123,6 @@ func (s *Server) CreateIndex() error { return err } - println(string(d)) - ifile := filepath.Join(s.docroot, "index.yaml") return ioutil.WriteFile(ifile, d, 0755) } @@ -148,11 +146,23 @@ func (s *Server) URL() string { 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. func setTestingRepository(helmhome, name, url string) error { rf := repo.NewRepoFile() rf.Add(&repo.Entry{Name: name, URL: url}) os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) dest := filepath.Join(helmhome, "repository/repositories.yaml") + return rf.WriteFile(dest, 0644) }