Skip to content

Commit

Permalink
feat(helm): remove the requirement that fetch/install need version
Browse files Browse the repository at this point in the history
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 helm#1198
  • Loading branch information
technosophos authored and Ville Aikas committed Oct 17, 2016
1 parent d61f689 commit e429aaf
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 106 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -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.

Expand Down
97 changes: 71 additions & 26 deletions cmd/helm/downloader/chart_downloader.go
Expand Up @@ -24,6 +24,7 @@ import (
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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) {
Expand Down
37 changes: 32 additions & 5 deletions cmd/helm/downloader/chart_downloader_test.go
Expand Up @@ -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},
Expand All @@ -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
Expand Down Expand Up @@ -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.")
}
Expand All @@ -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)
}
}
}
69 changes: 47 additions & 22 deletions cmd/helm/downloader/manager.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion cmd/helm/downloader/manager_test.go
Expand Up @@ -20,7 +20,6 @@ import (
"testing"

"k8s.io/helm/cmd/helm/helmpath"
//"k8s.io/helm/pkg/repo"
)

func TestVersionEquals(t *testing.T) {
Expand Down
@@ -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: ""

0 comments on commit e429aaf

Please sign in to comment.