Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(helm): remove the requirement that fetch/install need version #1293

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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: ""
Loading