Merge pull request #1766 from gravitational/http-client-tls

Http client with TLS
pull/1814/head
Matt Butcher 9 years ago committed by GitHub
commit 6d6e88a86e

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -76,12 +75,12 @@ type ChartDownloader struct {
// Returns a string path to the location where the file was downloaded and a verification // 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. // (if provenance was verified), or an error if something bad happened.
func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
// resolve URL u, r, err := c.ResolveChartVersion(ref, version)
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(), r)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@ -95,8 +94,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
// If provenance is requested, verify it. // If provenance is requested, verify it.
ver := &provenance.Verification{} ver := &provenance.Verification{}
if c.Verify > VerifyNever { if c.Verify > VerifyNever {
body, err := download(u.String()+".prov", r)
body, err := download(u.String() + ".prov")
if err != nil { if err != nil {
if c.Verify == VerifyAlways { if c.Verify == VerifyAlways {
return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov")
@ -132,63 +130,75 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
// * If version is non-empty, this will return the URL for that version // * 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 version is empty, this will return the URL for the latest version
// * If no version can be found, an error is returned // * If no version can be found, an error is returned
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, *repo.ChartRepository, error) {
// See if it's already a full URL. u, err := url.Parse(ref)
// FIXME: Why do we use url.ParseRequestURI instead of url.Parse? if err != nil {
u, err := url.ParseRequestURI(ref) return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref)
if err == nil {
// If it has a scheme and host and path, it's a full URL
if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
return u, nil
}
return u, fmt.Errorf("invalid chart url format: %s", ref)
} }
r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
if err != nil { if err != nil {
return u, err return nil, nil, err
} }
// See if it's of the form: repo/path_to_chart var (
p := strings.SplitN(ref, "/", 2) chartName string
if len(p) < 2 { rc *repo.Entry
return u, fmt.Errorf("invalid chart url format: %s", ref) )
if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
// If it has a scheme and host and path, it's a full URL
p := strings.SplitN(strings.TrimLeft(u.Path, "/"), "-", 2)
if len(p) < 2 {
return nil, nil, fmt.Errorf("Seems that chart path is not in form of repo_url/path_to_chart, got: %s", u)
}
chartName = p[0]
u.Path = ""
rc, err = pickChartRepositoryConfigByURL(u.String(), rf.Repositories)
if err != nil {
return nil, nil, err
}
} else {
// See if it's of the form: repo/path_to_chart
p := strings.SplitN(u.Path, "/", 2)
if len(p) < 2 {
return nil, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
}
repoName := p[0]
chartName = p[1]
rc, err = pickChartRepositoryConfigByName(repoName, rf.Repositories)
if err != nil {
return nil, nil, err
}
} }
repoName := p[0] r, err := repo.NewChartRepository(rc)
chartName := p[1]
rf, err := findRepoEntry(repoName, r.Repositories)
if err != nil { if err != nil {
return u, err return nil, nil, 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. // Next, we need to load the index, and actually look up the chart.
i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(repoName)) i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
if err != nil { if err != nil {
return u, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) return nil, nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
} }
cv, err := i.Get(chartName, version) cv, err := i.Get(chartName, version)
if err != nil { if err != nil {
return u, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, repoName, err) return nil, nil, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, r.Config.Name, err)
} }
if len(cv.URLs) == 0 { if len(cv.URLs) == 0 {
return u, fmt.Errorf("chart %q has no downloadable URLs", ref) return nil, nil, fmt.Errorf("chart %q has no downloadable URLs", ref)
} }
return url.Parse(cv.URLs[0])
}
func findRepoEntry(name string, repos []*repo.Entry) (*repo.Entry, error) { // TODO: Seems that picking first URL is not fully correct
for _, re := range repos { u, err = url.Parse(cv.URLs[0])
if re.Name == name { if err != nil {
return re, nil return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref)
}
} }
return nil, fmt.Errorf("no repo named %q", name)
return u, r, nil
} }
// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
@ -217,11 +227,11 @@ func VerifyChart(path string, keyring string) (*provenance.Verification, error)
return sig.Verify(path, provfile) return sig.Verify(path, provfile)
} }
// download performs a simple HTTP Get and returns the body. // download performs a Get from repo.Getter and returns the body.
func download(href string) (*bytes.Buffer, error) { func download(href string, r repo.Getter) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
resp, err := http.Get(href) resp, err := r.Get(href)
if err != nil { if err != nil {
return buf, err return buf, err
} }
@ -241,3 +251,24 @@ func download(href string) (*bytes.Buffer, error) {
func isTar(filename string) bool { func isTar(filename string) bool {
return strings.ToLower(filepath.Ext(filename)) == ".tgz" return strings.ToLower(filepath.Ext(filename)) == ".tgz"
} }
func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) {
for _, rc := range cfgs {
if rc.Name == name {
if rc.URL == "" {
return nil, fmt.Errorf("no URL found for repository %s", name)
}
return rc, nil
}
}
return nil, fmt.Errorf("repo %s not found", name)
}
func pickChartRepositoryConfigByURL(u string, cfgs []*repo.Entry) (*repo.Entry, error) {
for _, rc := range cfgs {
if rc.URL == u {
return rc, nil
}
}
return nil, fmt.Errorf("repo with URL %s not found", u)
}

@ -37,9 +37,9 @@ func TestResolveChartRef(t *testing.T) {
{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: "full URL, with authentication", ref: "http://username:password@example.com/foo-1.2.3.tgz", expect: "http://username:password@example.com/foo-1.2.3.tgz"}, {name: "full URL, with authentication", ref: "http://username:password@example.com/foo-1.2.3.tgz", expect: "http://username:password@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, 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: "reference, version, testing repo", ref: "testing/alpine", version: "0.2.0", expect: "http://example.com/alpine-0.2.0.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", fail: true},
{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},
@ -51,7 +51,7 @@ func TestResolveChartRef(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
u, err := c.ResolveChartVersion(tt.ref, tt.version) u, _, err := c.ResolveChartVersion(tt.ref, tt.version)
if err != nil { if err != nil {
if tt.fail { if tt.fail {
continue continue
@ -84,7 +84,7 @@ func TestDownload(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
got, err := download(srv.URL) got, err := download(srv.URL, http.DefaultClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -105,7 +105,7 @@ func TestDownload(t *testing.T) {
u, _ := url.ParseRequestURI(basicAuthSrv.URL) u, _ := url.ParseRequestURI(basicAuthSrv.URL)
u.User = url.UserPassword("username", "password") u.User = url.UserPassword("username", "password")
got, err = download(u.String()) got, err = download(u.String(), http.DefaultClient)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -133,25 +133,43 @@ func TestIsTar(t *testing.T) {
} }
func TestDownloadTo(t *testing.T) { func TestDownloadTo(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-downloadto-") tmp, err := ioutil.TempDir("", "helm-downloadto-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(hh) defer os.RemoveAll(tmp)
dest := filepath.Join(hh, "dest") hh := helmpath.Home(tmp)
os.MkdirAll(dest, 0755) dest := filepath.Join(hh.String(), "dest")
configDirectories := []string{
hh.String(),
hh.Repository(),
hh.Cache(),
dest,
}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
if err := os.MkdirAll(p, 0755); err != nil {
t.Fatalf("Could not create %s: %s", p, err)
}
} else if !fi.IsDir() {
t.Fatalf("%s must be a directory", p)
}
}
// Set up a fake repo // Set up a fake repo
srv := repotest.NewServer(hh) srv := repotest.NewServer(tmp)
defer srv.Stop() defer srv.Stop()
if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil {
t.Error(err) t.Error(err)
return return
} }
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
c := ChartDownloader{ c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"), HelmHome: hh,
Out: os.Stderr, Out: os.Stderr,
Verify: VerifyAlways, Verify: VerifyAlways,
Keyring: "testdata/helm-test-key.pub", Keyring: "testdata/helm-test-key.pub",
@ -178,25 +196,43 @@ func TestDownloadTo(t *testing.T) {
} }
func TestDownloadTo_VerifyLater(t *testing.T) { func TestDownloadTo_VerifyLater(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-downloadto-") tmp, err := ioutil.TempDir("", "helm-downloadto-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(hh) defer os.RemoveAll(tmp)
dest := filepath.Join(hh, "dest") hh := helmpath.Home(tmp)
os.MkdirAll(dest, 0755) dest := filepath.Join(hh.String(), "dest")
configDirectories := []string{
hh.String(),
hh.Repository(),
hh.Cache(),
dest,
}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
if err := os.MkdirAll(p, 0755); err != nil {
t.Fatalf("Could not create %s: %s", p, err)
}
} else if !fi.IsDir() {
t.Fatalf("%s must be a directory", p)
}
}
// Set up a fake repo // Set up a fake repo
srv := repotest.NewServer(hh) srv := repotest.NewServer(tmp)
defer srv.Stop() defer srv.Stop()
if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil {
t.Error(err) t.Error(err)
return return
} }
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
c := ChartDownloader{ c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"), HelmHome: hh,
Out: os.Stderr, Out: os.Stderr,
Verify: VerifyLater, Verify: VerifyLater,
} }

@ -35,6 +35,7 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/urlutil"
) )
// Manager handles the lifecycle of fetching, resolving, and storing dependencies. // Manager handles the lifecycle of fetching, resolving, and storing dependencies.
@ -226,7 +227,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
found = true found = true
} else { } else {
for _, repo := range repos { for _, repo := range repos {
if urlsAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
found = true found = true
} }
} }
@ -258,7 +259,7 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
found := false found := false
for _, repo := range repos { for _, repo := range repos {
if urlsAreEqual(repo.URL, dd.Repository) { if urlutil.Equal(repo.URL, dd.Repository) {
found = true found = true
reposMap[dd.Name] = repo.Name reposMap[dd.Name] = repo.Name
break break
@ -283,53 +284,35 @@ func (m *Manager) UpdateRepositories() error {
repos := rf.Repositories repos := rf.Repositories
if len(repos) > 0 { if len(repos) > 0 {
// This prints warnings straight to out. // This prints warnings straight to out.
m.parallelRepoUpdate(repos) if err := m.parallelRepoUpdate(repos); err != nil {
return err
}
} }
return nil return nil
} }
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) { func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
out := m.Out out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup var wg sync.WaitGroup
for _, re := range repos { for _, c := range repos {
r, err := repo.NewChartRepository(c)
if err != nil {
return err
}
wg.Add(1) wg.Add(1)
go func(n, u string) { go func(r *repo.ChartRepository) {
if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil { if err := r.DownloadIndexFile(); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err) fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err)
} else { } else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
} }
wg.Done() wg.Done()
}(re.Name, re.URL) }(r)
} }
wg.Wait() wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
} return nil
// urlsAreEqual normalizes two URLs and then compares for equality.
//
// TODO: This and the urlJoin functions should really be moved to a 'urlutil' package.
func urlsAreEqual(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
a = filepath.Clean(a)
b = filepath.Clean(b)
// If urls are paths, return true only if they are an exact match
return a == b
}
bu, err := url.Parse(b)
if err != nil {
return false
}
for _, u := range []*url.URL{au, bu} {
if u.Path == "" {
u.Path = "/"
}
u.Path = filepath.Clean(u.Path)
}
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.
@ -342,7 +325,7 @@ func urlsAreEqual(a, b string) bool {
// 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 urlutil.Equal(repoURL, cr.Config.URL) {
entry, err := findEntryByName(name, cr) entry, err := findEntryByName(name, cr)
if err != nil { if err != nil {
return "", err return "", err
@ -439,8 +422,9 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
return indices, err return indices, err
} }
// TODO: use constructor
cr := &repo.ChartRepository{ cr := &repo.ChartRepository{
URL: re.URL, Config: re,
IndexFile: index, IndexFile: index,
} }
indices[lname] = cr indices[lname] = cr

@ -135,27 +135,3 @@ func TestGetRepoNames(t *testing.T) {
} }
} }
} }
func TestUrlsAreEqual(t *testing.T) {
for _, tt := range []struct {
a, b string
match bool
}{
{"http://example.com", "http://example.com", true},
{"http://example.com", "http://another.example.com", false},
{"https://example.com", "https://example.com", true},
{"http://example.com/", "http://example.com", true},
{"https://example.com", "http://example.com", false},
{"http://example.com/foo", "http://example.com/foo/", true},
{"http://example.com/foo//", "http://example.com/foo/", true},
{"http://example.com/./foo/", "http://example.com/foo/", true},
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
{"/foo", "/foo", true},
{"/foo", "/foo/", true},
{"/foo/.", "/foo/", true},
} {
if tt.match != urlsAreEqual(tt.a, tt.b) {
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
}
}
}

@ -0,0 +1,15 @@
apiVersion: v1
entries:
foo:
- name: foo
description: Foo Chart
engine: gotpl
home: https://k8s.io/helm
keywords: []
maintainers: []
sources:
- https://github.com/kubernetes/charts
urls:
- http://username:password@example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d

@ -0,0 +1,15 @@
apiVersion: v1
entries:
foo:
- name: foo
description: Foo Chart
engine: gotpl
home: https://k8s.io/helm
keywords: []
maintainers: []
sources:
- https://github.com/kubernetes/charts
urls:
- https://example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d

@ -28,3 +28,16 @@ entries:
maintainers: [] maintainers: []
engine: "" engine: ""
icon: "" icon: ""
foo:
- name: foo
description: Foo Chart
engine: gotpl
home: https://k8s.io/helm
keywords: []
maintainers: []
sources:
- https://github.com/kubernetes/charts
urls:
- http://example.com/foo-1.2.3.tgz
version: 1.2.3
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d

@ -2,5 +2,9 @@ apiVersion: v1
repositories: repositories:
- name: testing - name: testing
url: "http://example.com" url: "http://example.com"
- name: testing-https
url: "https://example.com"
- name: testing-basicauth
url: "http://username:password@example.com"
- name: kubernetes-charts - name: kubernetes-charts
url: "http://example.com/charts" url: "http://example.com/charts"

@ -92,7 +92,6 @@ func newFetchCmd(out io.Writer) *cobra.Command {
} }
func (f *fetchCmd) run() error { func (f *fetchCmd) run() error {
pname := f.chartRef
c := downloader.ChartDownloader{ c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()), HelmHome: helmpath.Home(homePath()),
Out: f.out, Out: f.out,
@ -118,7 +117,7 @@ func (f *fetchCmd) run() error {
defer os.RemoveAll(dest) defer os.RemoveAll(dest)
} }
saved, v, err := c.DownloadTo(pname, f.version, dest) saved, v, err := c.DownloadTo(f.chartRef, f.version, dest)
if err != nil { if err != nil {
return err return err
} }

@ -55,7 +55,7 @@ To dump a manifest containing the Tiller deployment YAML, combine the
const ( const (
stableRepository = "stable" stableRepository = "stable"
localRepository = "local" localRepository = "local"
stableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com/" stableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com"
// This is the IPv4 loopback, not localhost, because we have to force IPv4 // This is the IPv4 loopback, not localhost, because we have to force IPv4
// for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410 // for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410
localRepositoryURL = "http://127.0.0.1:8879/charts" localRepositoryURL = "http://127.0.0.1:8879/charts"
@ -104,7 +104,6 @@ func newInitCmd(out io.Writer) *cobra.Command {
// runInit initializes local config and installs tiller to Kubernetes Cluster // runInit initializes local config and installs tiller to Kubernetes Cluster
func (i *initCmd) run() error { func (i *initCmd) run() error {
if flagDebug { if flagDebug {
m, err := installer.DeploymentManifest(i.namespace, i.image, i.canary) m, err := installer.DeploymentManifest(i.namespace, i.image, i.canary)
if err != nil { if err != nil {
@ -112,13 +111,21 @@ func (i *initCmd) run() error {
} }
fmt.Fprintln(i.out, m) fmt.Fprintln(i.out, m)
} }
if i.dryRun { if i.dryRun {
return nil return nil
} }
if err := ensureHome(i.home, i.out); err != nil { if err := ensureDirectories(i.home, i.out); err != nil {
return err return err
} }
if err := ensureDefaultRepos(i.home, i.out); err != nil {
return err
}
if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil {
return err
}
fmt.Fprintf(i.out, "$HELM_HOME has been configured at %s.\n", helmHome)
if !i.clientOnly { if !i.clientOnly {
if i.kubeClient == nil { if i.kubeClient == nil {
@ -147,15 +154,23 @@ func (i *initCmd) run() error {
} else { } else {
fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set") fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set")
} }
fmt.Fprintln(i.out, "Happy Helming!") fmt.Fprintln(i.out, "Happy Helming!")
return nil return nil
} }
// ensureHome checks to see if $HELM_HOME exists // ensureDirectories checks to see if $HELM_HOME exists
// //
// If $HELM_HOME does not exist, this function will create it. // If $HELM_HOME does not exist, this function will create it.
func ensureHome(home helmpath.Home, out io.Writer) error { func ensureDirectories(home helmpath.Home, out io.Writer) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()} configDirectories := []string{
home.String(),
home.Repository(),
home.Cache(),
home.LocalRepository(),
home.Plugins(),
home.Starters(),
}
for _, p := range configDirectories { for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil { if fi, err := os.Stat(p); err != nil {
fmt.Fprintf(out, "Creating %s \n", p) fmt.Fprintf(out, "Creating %s \n", p)
@ -167,50 +182,79 @@ func ensureHome(home helmpath.Home, out io.Writer) error {
} }
} }
return nil
}
func ensureDefaultRepos(home helmpath.Home, out io.Writer) error {
repoFile := home.RepositoryFile() repoFile := home.RepositoryFile()
if fi, err := os.Stat(repoFile); err != nil { if fi, err := os.Stat(repoFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", repoFile) fmt.Fprintf(out, "Creating %s \n", repoFile)
r := repo.NewRepoFile() f := repo.NewRepoFile()
r.Add(&repo.Entry{ sr, err := initStableRepo(home.CacheIndex(stableRepository))
Name: stableRepository, if err != nil {
URL: stableRepositoryURL,
Cache: "stable-index.yaml",
}, &repo.Entry{
Name: localRepository,
URL: localRepositoryURL,
Cache: "local-index.yaml",
})
if err := r.WriteFile(repoFile, 0644); err != nil {
return err return err
} }
cif := home.CacheIndex(stableRepository) lr, err := initLocalRepo(home.LocalRepository(localRepoIndexFilePath), home.CacheIndex("local"))
if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil { if err != nil {
fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm repo update')\n", stableRepository, err) return err
}
f.Add(sr)
f.Add(lr)
if err := f.WriteFile(repoFile, 0644); err != nil {
return err
} }
} else if fi.IsDir() { } else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", repoFile) return fmt.Errorf("%s must be a file, not a directory", repoFile)
} }
if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate { return nil
fmt.Fprintln(out, "Updating repository file format...") }
if err := r.WriteFile(repoFile, 0644); err != nil {
return err func initStableRepo(cacheFile string) (*repo.Entry, error) {
} c := repo.Entry{
Name: stableRepository,
URL: stableRepositoryURL,
Cache: cacheFile,
}
r, err := repo.NewChartRepository(&c)
if err != nil {
return nil, err
} }
localRepoIndexFile := home.LocalRepository(localRepoIndexFilePath) if err := r.DownloadIndexFile(); err != nil {
if fi, err := os.Stat(localRepoIndexFile); err != nil { return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error())
fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile) }
return &c, nil
}
func initLocalRepo(indexFile, cacheFile string) (*repo.Entry, error) {
if fi, err := os.Stat(indexFile); err != nil {
i := repo.NewIndexFile() i := repo.NewIndexFile()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil { if err := i.WriteFile(indexFile, 0644); err != nil {
return err return nil, err
} }
//TODO: take this out and replace with helm update functionality //TODO: take this out and replace with helm update functionality
os.Symlink(localRepoIndexFile, home.CacheIndex("local")) os.Symlink(indexFile, cacheFile)
} else if fi.IsDir() { } else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) return nil, fmt.Errorf("%s must be a file, not a directory", indexFile)
}
return &repo.Entry{
Name: localRepository,
URL: localRepositoryURL,
Cache: cacheFile,
}, nil
}
func ensureRepoFileFormat(file string, out io.Writer) error {
r, err := repo.LoadRepositoriesFile(file)
if err == repo.ErrRepoOutOfDate {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(file, 0644); err != nil {
return err
}
} }
fmt.Fprintf(out, "$HELM_HOME has been configured at %s.\n", helmHome)
return nil return nil
} }

@ -170,7 +170,13 @@ func TestEnsureHome(t *testing.T) {
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
hh := helmpath.Home(home) hh := helmpath.Home(home)
helmHome = home helmHome = home
if err := ensureHome(hh, b); err != nil { if err := ensureDirectories(hh, b); err != nil {
t.Error(err)
}
if err := ensureDefaultRepos(hh, b); err != nil {
t.Error(err)
}
if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil {
t.Error(err) t.Error(err)
} }

@ -19,8 +19,6 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -32,8 +30,13 @@ type repoAddCmd struct {
name string name string
url string url string
home helmpath.Home home helmpath.Home
out io.Writer
noupdate bool noupdate bool
certFile string
keyFile string
caFile string
out io.Writer
} }
func newRepoAddCmd(out io.Writer) *cobra.Command { func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -56,73 +59,54 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
return add.run() return add.run()
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered") f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered")
f.StringVar(&add.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&add.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&add.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
return cmd return cmd
} }
func (a *repoAddCmd) run() error { func (a *repoAddCmd) run() error {
var err error if err := addRepository(a.name, a.url, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil {
if a.noupdate {
err = addRepository(a.name, a.url, a.home)
} else {
err = updateRepository(a.name, a.url, a.home)
}
if err != nil {
return err return err
} }
fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name)
return nil return nil
} }
func addRepository(name, url string, home helmpath.Home) error { func addRepository(name, url string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error {
cif := home.CacheIndex(name)
if err := repo.DownloadIndexFile(name, url, cif); err != nil {
return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
}
return insertRepoLine(name, url, home)
}
func insertRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil { if err != nil {
return err return err
} }
if f.Has(name) { if noUpdate && f.Has(name) {
return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name) return fmt.Errorf("The repository name you provided (%s) already exists. Please specify a different name.", name)
} }
f.Add(&repo.Entry{
Name: name,
URL: strings.TrimSuffix(url, "/"),
Cache: filepath.Base(cif),
})
return f.WriteFile(home.RepositoryFile(), 0644)
}
func updateRepository(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name) cif := home.CacheIndex(name)
if err := repo.DownloadIndexFile(name, url, cif); err != nil { c := repo.Entry{
return err Name: name,
Cache: cif,
URL: url,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
} }
return updateRepoLine(name, url, home) r, err := repo.NewChartRepository(&c)
}
func updateRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil { if err != nil {
return err return err
} }
f.Update(&repo.Entry{ if err := r.DownloadIndexFile(); err != nil {
Name: name, return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
URL: url, }
Cache: filepath.Base(cif),
}) f.Update(&c)
return f.WriteFile(home.RepositoryFile(), 0666) return f.WriteFile(home.RepositoryFile(), 0644)
} }

@ -80,7 +80,7 @@ func TestRepoAdd(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := addRepository(testName, ts.URL(), hh); err != nil { if err := addRepository(testName, ts.URL(), hh, "", "", "", true); err != nil {
t.Error(err) t.Error(err)
} }
@ -93,11 +93,11 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile())
} }
if err := updateRepository(testName, ts.URL(), hh); err != nil { if err := addRepository(testName, ts.URL(), hh, "", "", "", false); err != nil {
t.Errorf("Repository was not updated: %s", err) t.Errorf("Repository was not updated: %s", err)
} }
if err := addRepository(testName, ts.URL(), hh); err == nil { if err := addRepository(testName, ts.URL(), hh, "", "", "", false); err != nil {
t.Errorf("Duplicate repository name was added") t.Errorf("Duplicate repository name was added")
} }
} }

@ -24,24 +24,33 @@ import (
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
) )
func TestRepoRemove(t *testing.T) { func TestRepoRemove(t *testing.T) {
testURL := "https://test-url.com" ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
b := bytes.NewBuffer(nil)
home, err := tempHelmHome(t)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.Remove(home)
hh := helmpath.Home(home) oldhome := homePath()
helmHome = thome
hh := helmpath.Home(thome)
defer func() {
ts.Stop()
helmHome = oldhome
os.Remove(thome)
}()
if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err)
}
b := bytes.NewBuffer(nil)
if err := removeRepoLine(b, testName, hh); err == nil { if err := removeRepoLine(b, testName, hh); err == nil {
t.Errorf("Expected error removing %s, but did not get one.", testName) t.Errorf("Expected error removing %s, but did not get one.", testName)
} }
if err := insertRepoLine(testName, testURL, hh); err != nil { if err := addRepository(testName, ts.URL(), hh, "", "", "", true); err != nil {
t.Error(err) t.Error(err)
} }

@ -36,10 +36,14 @@ Information is cached locally, where it is used by commands like 'helm search'.
future releases. future releases.
` `
var (
errNoRepositories = errors.New("no repositories found. You must add one before updating")
)
type repoUpdateCmd struct { type repoUpdateCmd struct {
update func([]*repo.Entry, bool, io.Writer, helmpath.Home) update func([]*repo.ChartRepository, io.Writer)
out io.Writer
home helmpath.Home home helmpath.Home
out io.Writer
} }
func newRepoUpdateCmd(out io.Writer) *cobra.Command { func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -67,31 +71,39 @@ func (u *repoUpdateCmd) run() error {
} }
if len(f.Repositories) == 0 { if len(f.Repositories) == 0 {
return errors.New("no repositories found. You must add one before updating") return errNoRepositories
}
var repos []*repo.ChartRepository
for _, cfg := range f.Repositories {
r, err := repo.NewChartRepository(cfg)
if err != nil {
return err
}
repos = append(repos, r)
} }
u.update(f.Repositories, flagDebug, u.out, u.home) u.update(repos, u.out)
return nil return nil
} }
func updateCharts(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) { func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup var wg sync.WaitGroup
for _, re := range repos { for _, re := range repos {
wg.Add(1) wg.Add(1)
go func(n, u string) { go func(re *repo.ChartRepository) {
defer wg.Done() defer wg.Done()
if n == localRepository { if re.Config.Name == localRepository {
// We skip local because the indices are symlinked. fmt.Fprintf(out, "...Skip %s chart repository", re.Config.Name)
return return
} }
err := repo.DownloadIndexFile(n, u, home.CacheIndex(n)) err := re.DownloadIndexFile()
if err != nil { if err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err) fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
} else { } else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n) fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
} }
}(re.Name, re.URL) }(re)
} }
wg.Wait() wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ")

@ -43,15 +43,15 @@ func TestUpdateCmd(t *testing.T) {
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
// Instead of using the HTTP updater, we provide our own for this test. // Instead of using the HTTP updater, we provide our own for this test.
// The TestUpdateCharts test verifies the HTTP behavior independently. // The TestUpdateCharts test verifies the HTTP behavior independently.
updater := func(repos []*repo.Entry, verbose bool, out io.Writer, home helmpath.Home) { updater := func(repos []*repo.ChartRepository, out io.Writer) {
for _, re := range repos { for _, re := range repos {
fmt.Fprintln(out, re.Name) fmt.Fprintln(out, re.Config.Name)
} }
} }
uc := &repoUpdateCmd{ uc := &repoUpdateCmd{
out: out,
update: updater, update: updater,
home: helmpath.Home(thome), home: helmpath.Home(thome),
out: out,
} }
if err := uc.run(); err != nil { if err := uc.run(); err != nil {
t.Fatal(err) t.Fatal(err)
@ -63,33 +63,40 @@ func TestUpdateCmd(t *testing.T) {
} }
func TestUpdateCharts(t *testing.T) { func TestUpdateCharts(t *testing.T) {
srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*") ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
oldhome := homePath() oldhome := homePath()
helmHome = thome helmHome = thome
hh := helmpath.Home(thome)
defer func() { defer func() {
srv.Stop() ts.Stop()
helmHome = oldhome helmHome = oldhome
os.Remove(thome) os.Remove(thome)
}() }()
if err := ensureTestHome(helmpath.Home(thome), t); err != nil { if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err) t.Fatal(err)
} }
buf := bytes.NewBuffer(nil) r, err := repo.NewChartRepository(&repo.Entry{
repos := []*repo.Entry{ Name: "charts",
{Name: "charts", URL: srv.URL()}, URL: ts.URL(),
Cache: hh.CacheIndex("charts"),
})
if err != nil {
t.Error(err)
} }
updateCharts(repos, false, buf, helmpath.Home(thome))
got := buf.String() b := bytes.NewBuffer(nil)
updateCharts([]*repo.ChartRepository{r}, b)
got := b.String()
if strings.Contains(got, "Unable to get an update") { if strings.Contains(got, "Unable to get an update") {
t.Errorf("Failed to get a repo: %q", got) t.Errorf("Failed to get a repo: %q", got)
} }
if !strings.Contains(got, "Update Complete.") { if !strings.Contains(got, "Update Complete.") {
t.Errorf("Update was not successful") t.Error("Update was not successful")
} }
} }

@ -0,0 +1,190 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package repo // import "k8s.io/helm/pkg/repo"
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/tlsutil"
"k8s.io/helm/pkg/urlutil"
)
// Entry represents a collection of parameters for chart repository
type Entry struct {
Name string `json:"name"`
Cache string `json:"cache"`
URL string `json:"url"`
CertFile string `json:"certFile"`
KeyFile string `json:"keyFile"`
CAFile string `json:"caFile"`
}
// ChartRepository represents a chart repository
type ChartRepository struct {
Config *Entry
ChartPaths []string
IndexFile *IndexFile
Client *http.Client
}
// Getter is an interface to support GET to the specified URL.
type Getter interface {
Get(url string) (*http.Response, error)
}
// NewChartRepository constructs ChartRepository
func NewChartRepository(cfg *Entry) (*ChartRepository, error) {
var client *http.Client
if cfg.CertFile != "" && cfg.KeyFile != "" && cfg.CAFile != "" {
tlsConf, err := tlsutil.NewClientTLS(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
if err != nil {
return nil, fmt.Errorf("can't create TLS config for client: %s", err.Error())
}
tlsConf.BuildNameToCertificate()
sni, err := urlutil.ExtractHostname(cfg.URL)
if err != nil {
return nil, err
}
tlsConf.ServerName = sni
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConf,
},
}
} else {
client = http.DefaultClient
}
return &ChartRepository{
Config: cfg,
IndexFile: NewIndexFile(),
Client: client,
}, nil
}
// Get issues a GET using configured client to the specified URL.
func (r *ChartRepository) Get(url string) (*http.Response, error) {
resp, err := r.Client.Get(url)
if err != nil {
return nil, err
}
return resp, nil
}
// Load loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
func (r *ChartRepository) Load() error {
dirInfo, err := os.Stat(r.Config.Name)
if err != nil {
return err
}
if !dirInfo.IsDir() {
return fmt.Errorf("%q is not a directory", r.Config.Name)
}
// FIXME: Why are we recursively walking directories?
// FIXME: Why are we not reading the repositories.yaml to figure out
// what repos to use?
filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
r.ChartPaths = append(r.ChartPaths, path)
}
}
return nil
})
return nil
}
// DownloadIndexFile fetches the index from a repository.
func (r *ChartRepository) DownloadIndexFile() error {
var indexURL string
indexURL = strings.TrimSuffix(r.Config.URL, "/") + "/index.yaml"
resp, err := r.Get(indexURL)
if err != nil {
return err
}
defer resp.Body.Close()
index, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if _, err := loadIndex(index); err != nil {
return err
}
return ioutil.WriteFile(r.Config.Cache, index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error {
err := r.generateIndex()
if err != nil {
return err
}
return r.saveIndexFile()
}
func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(r.IndexFile)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
}
func (r *ChartRepository) generateIndex() error {
for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path)
if err != nil {
return err
}
digest, err := provenance.DigestFile(path)
if err != nil {
return err
}
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
}
// TODO: If a chart exists, but has a different Digest, should we error?
}
r.IndexFile.SortEntries()
return nil
}

@ -0,0 +1,185 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package repo
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const (
testRepository = "testdata/repository"
testURL = "http://example-charts.com"
)
func TestLoadChartRepository(t *testing.T) {
r, err := NewChartRepository(&Entry{
Name: testRepository,
URL: testURL,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
}
if err := r.Load(); err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
paths := []string{
filepath.Join(testRepository, "frobnitz-1.2.3.tgz"),
filepath.Join(testRepository, "sprocket-1.1.0.tgz"),
filepath.Join(testRepository, "sprocket-1.2.0.tgz"),
}
if r.Config.Name != testRepository {
t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name)
}
if !reflect.DeepEqual(r.ChartPaths, paths) {
t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths)
}
if r.Config.URL != testURL {
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL)
}
}
func TestIndex(t *testing.T) {
r, err := NewChartRepository(&Entry{
Name: testRepository,
URL: testURL,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
}
if err := r.Load(); err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
err = r.Index()
if err != nil {
t.Errorf("Error performing index: %v\n", err)
}
tempIndexPath := filepath.Join(testRepository, indexPath)
actual, err := LoadIndexFile(tempIndexPath)
defer os.Remove(tempIndexPath) // clean up
if err != nil {
t.Errorf("Error loading index file %v", err)
}
verifyIndex(t, actual)
// Re-index and test again.
err = r.Index()
if err != nil {
t.Errorf("Error performing re-index: %s\n", err)
}
second, err := LoadIndexFile(tempIndexPath)
if err != nil {
t.Errorf("Error re-loading index file %v", err)
}
verifyIndex(t, second)
}
func verifyIndex(t *testing.T, actual *IndexFile) {
var empty time.Time
if actual.Generated == empty {
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
}
if actual.APIVersion != APIVersionV1 {
t.Error("Expected v1 API")
}
entries := actual.Entries
if numEntries := len(entries); numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
expects := map[string]ChartVersions{
"frobnitz": {
{
Metadata: &chart.Metadata{
Name: "frobnitz",
Version: "1.2.3",
},
},
},
"sprocket": {
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.2.0",
},
},
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.1.0",
},
},
},
}
for name, versions := range expects {
got, ok := entries[name]
if !ok {
t.Errorf("Could not find %q entry", name)
continue
}
if len(versions) != len(got) {
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
continue
}
for i, e := range versions {
g := got[i]
if e.Name != g.Name {
t.Errorf("Expected %q, got %q", e.Name, g.Name)
}
if e.Version != g.Version {
t.Errorf("Expected %q, got %q", e.Version, g.Version)
}
if len(g.Keywords) != 3 {
t.Error("Expected 3 keyrwords.")
}
if len(g.Maintainers) != 2 {
t.Error("Expected 2 maintainers.")
}
if g.Created == empty {
t.Error("Expected created to be non-empty")
}
if g.Description == "" {
t.Error("Expected description to be non-empty")
}
if g.Home == "" {
t.Error("Expected home to be non-empty")
}
if g.Digest == "" {
t.Error("Expected digest to be non-empty")
}
if len(g.URLs) != 1 {
t.Error("Expected exactly 1 URL")
}
}
}
}

@ -21,10 +21,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http"
"net/url"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -36,6 +33,7 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/urlutil"
) )
var indexPath = "index.yaml" var indexPath = "index.yaml"
@ -94,6 +92,15 @@ func NewIndexFile() *IndexFile {
} }
} }
// LoadIndexFile takes a file at the given path and returns an IndexFile object
func LoadIndexFile(path string) (*IndexFile, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return loadIndex(b)
}
// Add adds a file to the index // Add adds a file to the index
// This can leave the index in an unsorted state // This can leave the index in an unsorted state
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
@ -101,7 +108,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if baseURL != "" { if baseURL != "" {
var err error var err error
_, file := filepath.Split(filename) _, file := filepath.Split(filename)
u, err = urlJoin(baseURL, file) u, err = urlutil.URLJoin(baseURL, file)
if err != nil { if err != nil {
u = filepath.Join(baseURL, file) u = filepath.Join(baseURL, file)
} }
@ -228,33 +235,10 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
return index, nil return index, nil
} }
// DownloadIndexFile fetches the index from a repository. // loadIndex loads an index file and does minimal validity checking.
func DownloadIndexFile(repoName, url, indexFilePath string) error {
var indexURL string
indexURL = strings.TrimSuffix(url, "/") + "/index.yaml"
resp, err := http.Get(indexURL)
if err != nil {
return err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if _, err := LoadIndex(b); err != nil {
return err
}
return ioutil.WriteFile(indexFilePath, b, 0644)
}
// LoadIndex loads an index file and does minimal validity checking.
// //
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func LoadIndex(data []byte) (*IndexFile, error) { func loadIndex(data []byte) (*IndexFile, error) {
i := &IndexFile{} i := &IndexFile{}
if err := yaml.Unmarshal(data, i); err != nil { if err := yaml.Unmarshal(data, i); err != nil {
return i, err return i, err
@ -312,30 +296,3 @@ func loadUnversionedIndex(data []byte) (*IndexFile, error) {
} }
return ni, nil return ni, nil
} }
// LoadIndexFile takes a file at the given path and returns an IndexFile object
func LoadIndexFile(path string) (*IndexFile, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return LoadIndex(b)
}
// 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
}
// 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
}

@ -67,7 +67,7 @@ func TestLoadIndex(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
i, err := LoadIndex(b) i, err := loadIndex(b)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -132,21 +132,30 @@ func TestDownloadIndexFile(t *testing.T) {
} }
defer os.RemoveAll(dirName) defer os.RemoveAll(dirName)
path := filepath.Join(dirName, testRepo+"-index.yaml") indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml")
if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil { r, err := NewChartRepository(&Entry{
Name: testRepo,
URL: srv.URL,
Cache: indexFilePath,
})
if err != nil {
t.Errorf("Problem creating chart repository from %s: %v", testRepo, err)
}
if err := r.DownloadIndexFile(); err != nil {
t.Errorf("%#v", err) t.Errorf("%#v", err)
} }
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(indexFilePath); err != nil {
t.Errorf("error finding created index file: %#v", err) t.Errorf("error finding created index file: %#v", err)
} }
b, err := ioutil.ReadFile(path) b, err := ioutil.ReadFile(indexFilePath)
if err != nil { if err != nil {
t.Errorf("error reading index file: %#v", err) t.Errorf("error reading index file: %#v", err)
} }
i, err := LoadIndex(b) i, err := loadIndex(b)
if err != nil { if err != nil {
t.Errorf("Index %q failed to parse: %s", testfile, err) t.Errorf("Index %q failed to parse: %s", testfile, err)
return return
@ -305,7 +314,6 @@ func TestLoadUnversionedIndex(t *testing.T) {
} }
func TestIndexAdd(t *testing.T) { func TestIndexAdd(t *testing.T) {
i := NewIndexFile() i := NewIndexFile()
i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890")
@ -325,24 +333,3 @@ func TestIndexAdd(t *testing.T) {
t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0])
} }
} }
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)
}
}
}

@ -21,35 +21,15 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings"
"time" "time"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
) )
// ErrRepoOutOfDate indicates that the repository file is out of date, but // ErrRepoOutOfDate indicates that the repository file is out of date, but
// is fixable. // is fixable.
var ErrRepoOutOfDate = errors.New("repository file is out of date") var ErrRepoOutOfDate = errors.New("repository file is out of date")
// ChartRepository represents a chart repository
type ChartRepository struct {
RootPath string
URL string // URL of repository
ChartPaths []string
IndexFile *IndexFile
}
// Entry represents one repo entry in a repositories listing.
type Entry struct {
Name string `json:"name"`
Cache string `json:"cache"`
URL string `json:"url"`
}
// RepoFile represents the repositories.yaml file in $HELM_HOME // RepoFile represents the repositories.yaml file in $HELM_HOME
type RepoFile struct { type RepoFile struct {
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`
@ -160,83 +140,3 @@ func (r *RepoFile) WriteFile(path string, perm os.FileMode) error {
} }
return ioutil.WriteFile(path, data, perm) return ioutil.WriteFile(path, data, perm)
} }
// LoadChartRepository loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
//
// This function evaluates the contents of the directory and
// returns a ChartRepository
func LoadChartRepository(dir, url string) (*ChartRepository, error) {
dirInfo, err := os.Stat(dir)
if err != nil {
return nil, err
}
if !dirInfo.IsDir() {
return nil, fmt.Errorf("%q is not a directory", dir)
}
r := &ChartRepository{RootPath: dir, URL: url}
// FIXME: Why are we recursively walking directories?
// FIXME: Why are we not reading the repositories.yaml to figure out
// what repos to use?
filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if !f.IsDir() {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
r.ChartPaths = append(r.ChartPaths, path)
}
}
return nil
})
return r, nil
}
func (r *ChartRepository) saveIndexFile() error {
index, err := yaml.Marshal(r.IndexFile)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file.
func (r *ChartRepository) Index() error {
err := r.generateIndex()
if err != nil {
return err
}
return r.saveIndexFile()
}
func (r *ChartRepository) generateIndex() error {
if r.IndexFile == nil {
r.IndexFile = NewIndexFile()
}
for _, path := range r.ChartPaths {
ch, err := chartutil.Load(path)
if err != nil {
return err
}
digest, err := provenance.DigestFile(path)
if err != nil {
return err
}
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.URL, digest)
}
// TODO: If a chart exists, but has a different Digest, should we error?
}
r.IndexFile.SortEntries()
return nil
}

@ -16,19 +16,9 @@ limitations under the License.
package repo package repo
import ( import "testing"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
const testRepositoriesFile = "testdata/repositories.yaml" const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository"
const testURL = "http://example-charts.com"
func TestRepoFile(t *testing.T) { func TestRepoFile(t *testing.T) {
rf := NewRepoFile() rf := NewRepoFile()
@ -68,7 +58,7 @@ func TestRepoFile(t *testing.T) {
} }
} }
func TestLoadRepositoriesFile(t *testing.T) { func TestNewRepositoriesFile(t *testing.T) {
expects := NewRepoFile() expects := NewRepoFile()
expects.Add( expects.Add(
&Entry{ &Entry{
@ -106,7 +96,7 @@ func TestLoadRepositoriesFile(t *testing.T) {
} }
} }
func TestLoadPreV1RepositoriesFile(t *testing.T) { func TestNewPreV1RepositoriesFile(t *testing.T) {
r, err := LoadRepositoriesFile("testdata/old-repositories.yaml") r, err := LoadRepositoriesFile("testdata/old-repositories.yaml")
if err != nil && err != ErrRepoOutOfDate { if err != nil && err != ErrRepoOutOfDate {
t.Fatal(err) t.Fatal(err)
@ -126,139 +116,3 @@ func TestLoadPreV1RepositoriesFile(t *testing.T) {
t.Errorf("expected the best charts ever. Got %#v", r.Repositories) t.Errorf("expected the best charts ever. Got %#v", r.Repositories)
} }
} }
func TestLoadChartRepository(t *testing.T) {
cr, err := LoadChartRepository(testRepository, testURL)
if err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
paths := []string{filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), filepath.Join(testRepository, "sprocket-1.1.0.tgz"), filepath.Join(testRepository, "sprocket-1.2.0.tgz")}
if cr.RootPath != testRepository {
t.Errorf("Expected %s as RootPath but got %s", testRepository, cr.RootPath)
}
if !reflect.DeepEqual(cr.ChartPaths, paths) {
t.Errorf("Expected %#v but got %#v\n", paths, cr.ChartPaths)
}
if cr.URL != testURL {
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, cr.URL)
}
}
func TestIndex(t *testing.T) {
cr, err := LoadChartRepository(testRepository, testURL)
if err != nil {
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
}
err = cr.Index()
if err != nil {
t.Errorf("Error performing index: %v\n", err)
}
tempIndexPath := filepath.Join(testRepository, indexPath)
actual, err := LoadIndexFile(tempIndexPath)
defer os.Remove(tempIndexPath) // clean up
if err != nil {
t.Errorf("Error loading index file %v", err)
}
verifyIndex(t, actual)
// Re-index and test again.
err = cr.Index()
if err != nil {
t.Errorf("Error performing re-index: %s\n", err)
}
second, err := LoadIndexFile(tempIndexPath)
if err != nil {
t.Errorf("Error re-loading index file %v", err)
}
verifyIndex(t, second)
}
func verifyIndex(t *testing.T, actual *IndexFile) {
var empty time.Time
if actual.Generated == empty {
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
}
if actual.APIVersion != APIVersionV1 {
t.Error("Expected v1 API")
}
entries := actual.Entries
if numEntries := len(entries); numEntries != 2 {
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
}
expects := map[string]ChartVersions{
"frobnitz": {
{
Metadata: &chart.Metadata{
Name: "frobnitz",
Version: "1.2.3",
},
},
},
"sprocket": {
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.2.0",
},
},
{
Metadata: &chart.Metadata{
Name: "sprocket",
Version: "1.1.0",
},
},
},
}
for name, versions := range expects {
got, ok := entries[name]
if !ok {
t.Errorf("Could not find %q entry", name)
continue
}
if len(versions) != len(got) {
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
continue
}
for i, e := range versions {
g := got[i]
if e.Name != g.Name {
t.Errorf("Expected %q, got %q", e.Name, g.Name)
}
if e.Version != g.Version {
t.Errorf("Expected %q, got %q", e.Version, g.Version)
}
if len(g.Keywords) != 3 {
t.Error("Expected 3 keyrwords.")
}
if len(g.Maintainers) != 2 {
t.Error("Expected 2 maintainers.")
}
if g.Created == empty {
t.Error("Expected created to be non-empty")
}
if g.Description == "" {
t.Error("Expected description to be non-empty")
}
if g.Home == "" {
t.Error("Expected home to be non-empty")
}
if g.Digest == "" {
t.Error("Expected digest to be non-empty")
}
if len(g.URLs) != 1 {
t.Error("Expected exactly 1 URL")
}
}
}
}

@ -24,6 +24,7 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
@ -69,7 +70,7 @@ func NewServer(docroot string) *Server {
} }
srv.start() srv.start()
// Add the testing repository as the only repo. // Add the testing repository as the only repo.
if err := setTestingRepository(docroot, "test", srv.URL()); err != nil { if err := setTestingRepository(helmpath.Home(docroot), "test", srv.URL()); err != nil {
panic(err) panic(err)
} }
return srv return srv
@ -158,11 +159,13 @@ func (s *Server) LinkIndices() error {
} }
// 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(home helmpath.Home, name, url string) error {
rf := repo.NewRepoFile() r := repo.NewRepoFile()
rf.Add(&repo.Entry{Name: name, URL: url}) r.Add(&repo.Entry{
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755) Name: name,
dest := filepath.Join(helmhome, "repository/repositories.yaml") URL: url,
Cache: home.CacheIndex(name),
return rf.WriteFile(dest, 0644) })
os.MkdirAll(filepath.Join(home.Repository(), name), 0755)
return r.WriteFile(home.RepositoryFile(), 0644)
} }

@ -0,0 +1,68 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tlsutil
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
)
// NewClientTLS returns tls.Config appropriate for client auth.
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
cert, err := CertFromFilePair(certFile, keyFile)
if err != nil {
return nil, err
}
cp, err := CertPoolFromFile(caFile)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{*cert},
RootCAs: cp,
}, nil
}
// CertPoolFromFile returns an x509.CertPool containing the certificates
// in the given PEM-encoded file.
// Returns an error if the file could not be read, a certificate could not
// be parsed, or if the file does not contain any certificates
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("can't read CA file: %v", filename)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("failed to append certificates from file: %s", filename)
}
return cp, nil
}
// CertFromFilePair returns an tls.Certificate containing the
// certificates public/private key pair from a pair of given PEM-encoded files.
// Returns an error if the file could not be read, a certificate could not
// be parsed, or if the file does not contain any certificates
func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, fmt.Errorf("can't load key pair from cert %s and key %s", certFile, keyFile)
}
return &cert, err
}

@ -0,0 +1,79 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package urlutil
import (
"net"
"net/url"
"path"
"path/filepath"
)
// 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
}
// 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
}
// Equal normalizes two URLs and then compares for equality.
func Equal(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
a = filepath.Clean(a)
b = filepath.Clean(b)
// If urls are paths, return true only if they are an exact match
return a == b
}
bu, err := url.Parse(b)
if err != nil {
return false
}
for _, u := range []*url.URL{au, bu} {
if u.Path == "" {
u.Path = "/"
}
u.Path = filepath.Clean(u.Path)
}
return au.String() == bu.String()
}
// ExtractHostname returns hostname from URL
func ExtractHostname(addr string) (string, error) {
u, err := url.Parse(addr)
if err != nil {
return "", err
}
host, _, err := net.SplitHostPort(u.Host)
if err != nil {
return "", err
}
return host, nil
}

@ -0,0 +1,64 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package urlutil
import "testing"
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)
}
}
}
func TestEqual(t *testing.T) {
for _, tt := range []struct {
a, b string
match bool
}{
{"http://example.com", "http://example.com", true},
{"http://example.com", "http://another.example.com", false},
{"https://example.com", "https://example.com", true},
{"http://example.com/", "http://example.com", true},
{"https://example.com", "http://example.com", false},
{"http://example.com/foo", "http://example.com/foo/", true},
{"http://example.com/foo//", "http://example.com/foo/", true},
{"http://example.com/./foo/", "http://example.com/foo/", true},
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
{"/foo", "/foo", true},
{"/foo", "/foo/", true},
{"/foo/.", "/foo/", true},
} {
if tt.match != Equal(tt.a, tt.b) {
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
}
}
}
Loading…
Cancel
Save