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

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

@ -21,7 +21,6 @@ import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
@ -76,12 +75,12 @@ type ChartDownloader struct {
// 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, version)
u, r, err := c.ResolveChartVersion(ref, version)
if err != nil {
return "", nil, err
}
data, err := download(u.String())
data, err := download(u.String(), r)
if err != nil {
return "", nil, err
}
@ -95,8 +94,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
// If provenance is requested, verify it.
ver := &provenance.Verification{}
if c.Verify > VerifyNever {
body, err := download(u.String() + ".prov")
body, err := download(u.String()+".prov", r)
if err != nil {
if c.Verify == VerifyAlways {
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 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
if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
return u, nil
}
return u, fmt.Errorf("invalid chart url format: %s", ref)
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, *repo.ChartRepository, error) {
u, err := url.Parse(ref)
if err != nil {
return nil, nil, 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 {
return u, err
return nil, nil, err
}
// See if it's of the form: repo/path_to_chart
p := strings.SplitN(ref, "/", 2)
if len(p) < 2 {
return u, fmt.Errorf("invalid chart url format: %s", ref)
var (
chartName string
rc *repo.Entry
)
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]
chartName := p[1]
rf, err := findRepoEntry(repoName, r.Repositories)
r, err := repo.NewChartRepository(rc)
if err != nil {
return u, err
}
if rf.URL == "" {
return u, fmt.Errorf("no URL found for repository %q", repoName)
return nil, nil, err
}
// 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 {
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)
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 {
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) {
for _, re := range repos {
if re.Name == name {
return re, nil
}
// TODO: Seems that picking first URL is not fully correct
u, err = url.Parse(cv.URLs[0])
if err != 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.
@ -217,11 +227,11 @@ func VerifyChart(path string, keyring string) (*provenance.Verification, error)
return sig.Verify(path, provfile)
}
// download performs a simple HTTP Get and returns the body.
func download(href string) (*bytes.Buffer, error) {
// download performs a Get from repo.Getter and returns the body.
func download(href string, r repo.Getter) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
resp, err := http.Get(href)
resp, err := r.Get(href)
if err != nil {
return buf, err
}
@ -241,3 +251,24 @@ func download(href string) (*bytes.Buffer, error) {
func isTar(filename string) bool {
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, 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, 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, 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: "invalid", ref: "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 {
u, err := c.ResolveChartVersion(tt.ref, tt.version)
u, _, err := c.ResolveChartVersion(tt.ref, tt.version)
if err != nil {
if tt.fail {
continue
@ -84,7 +84,7 @@ func TestDownload(t *testing.T) {
}))
defer srv.Close()
got, err := download(srv.URL)
got, err := download(srv.URL, http.DefaultClient)
if err != nil {
t.Fatal(err)
}
@ -105,7 +105,7 @@ func TestDownload(t *testing.T) {
u, _ := url.ParseRequestURI(basicAuthSrv.URL)
u.User = url.UserPassword("username", "password")
got, err = download(u.String())
got, err = download(u.String(), http.DefaultClient)
if err != nil {
t.Fatal(err)
}
@ -133,25 +133,43 @@ func TestIsTar(t *testing.T) {
}
func TestDownloadTo(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-downloadto-")
tmp, err := ioutil.TempDir("", "helm-downloadto-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
defer os.RemoveAll(tmp)
dest := filepath.Join(hh, "dest")
os.MkdirAll(dest, 0755)
hh := helmpath.Home(tmp)
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
srv := repotest.NewServer(hh)
srv := repotest.NewServer(tmp)
defer srv.Stop()
if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil {
t.Error(err)
return
}
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"),
HelmHome: hh,
Out: os.Stderr,
Verify: VerifyAlways,
Keyring: "testdata/helm-test-key.pub",
@ -178,25 +196,43 @@ func TestDownloadTo(t *testing.T) {
}
func TestDownloadTo_VerifyLater(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-downloadto-")
tmp, err := ioutil.TempDir("", "helm-downloadto-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
defer os.RemoveAll(tmp)
dest := filepath.Join(hh, "dest")
os.MkdirAll(dest, 0755)
hh := helmpath.Home(tmp)
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
srv := repotest.NewServer(hh)
srv := repotest.NewServer(tmp)
defer srv.Stop()
if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil {
t.Error(err)
return
}
if err := srv.LinkIndices(); err != nil {
t.Fatal(err)
}
c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"),
HelmHome: hh,
Out: os.Stderr,
Verify: VerifyLater,
}

@ -35,6 +35,7 @@ import (
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/urlutil"
)
// Manager handles the lifecycle of fetching, resolving, and storing dependencies.
@ -226,7 +227,7 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
found = true
} else {
for _, repo := range repos {
if urlsAreEqual(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) {
found = true
}
}
@ -258,7 +259,7 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string,
found := false
for _, repo := range repos {
if urlsAreEqual(repo.URL, dd.Repository) {
if urlutil.Equal(repo.URL, dd.Repository) {
found = true
reposMap[dd.Name] = repo.Name
break
@ -283,53 +284,35 @@ func (m *Manager) UpdateRepositories() error {
repos := rf.Repositories
if len(repos) > 0 {
// This prints warnings straight to out.
m.parallelRepoUpdate(repos)
if err := m.parallelRepoUpdate(repos); err != nil {
return err
}
}
return nil
}
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) {
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
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)
go func(n, u string) {
if err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n)); err != nil {
fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", n, u, err)
go func(r *repo.ChartRepository) {
if err := r.DownloadIndexFile(); err != nil {
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 {
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()
}(re.Name, re.URL)
}(r)
}
wg.Wait()
fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
}
// 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()
return nil
}
// 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.
func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos {
if urlsAreEqual(repoURL, cr.URL) {
if urlutil.Equal(repoURL, cr.Config.URL) {
entry, err := findEntryByName(name, cr)
if err != nil {
return "", err
@ -439,8 +422,9 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err
return indices, err
}
// TODO: use constructor
cr := &repo.ChartRepository{
URL: re.URL,
Config: re,
IndexFile: index,
}
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: []
engine: ""
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:
- name: testing
url: "http://example.com"
- name: testing-https
url: "https://example.com"
- name: testing-basicauth
url: "http://username:password@example.com"
- name: kubernetes-charts
url: "http://example.com/charts"

@ -92,7 +92,6 @@ func newFetchCmd(out io.Writer) *cobra.Command {
}
func (f *fetchCmd) run() error {
pname := f.chartRef
c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()),
Out: f.out,
@ -118,7 +117,7 @@ func (f *fetchCmd) run() error {
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 {
return err
}

@ -55,7 +55,7 @@ To dump a manifest containing the Tiller deployment YAML, combine the
const (
stableRepository = "stable"
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
// for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410
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
func (i *initCmd) run() error {
if flagDebug {
m, err := installer.DeploymentManifest(i.namespace, i.image, i.canary)
if err != nil {
@ -112,13 +111,21 @@ func (i *initCmd) run() error {
}
fmt.Fprintln(i.out, m)
}
if i.dryRun {
return nil
}
if err := ensureHome(i.home, i.out); err != nil {
if err := ensureDirectories(i.home, i.out); err != nil {
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.kubeClient == nil {
@ -147,15 +154,23 @@ func (i *initCmd) run() error {
} else {
fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set")
}
fmt.Fprintln(i.out, "Happy Helming!")
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.
func ensureHome(home helmpath.Home, out io.Writer) error {
configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()}
func ensureDirectories(home helmpath.Home, out io.Writer) error {
configDirectories := []string{
home.String(),
home.Repository(),
home.Cache(),
home.LocalRepository(),
home.Plugins(),
home.Starters(),
}
for _, p := range configDirectories {
if fi, err := os.Stat(p); err != nil {
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()
if fi, err := os.Stat(repoFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", repoFile)
r := repo.NewRepoFile()
r.Add(&repo.Entry{
Name: stableRepository,
URL: stableRepositoryURL,
Cache: "stable-index.yaml",
}, &repo.Entry{
Name: localRepository,
URL: localRepositoryURL,
Cache: "local-index.yaml",
})
if err := r.WriteFile(repoFile, 0644); err != nil {
f := repo.NewRepoFile()
sr, err := initStableRepo(home.CacheIndex(stableRepository))
if err != nil {
return err
}
cif := home.CacheIndex(stableRepository)
if err := repo.DownloadIndexFile(stableRepository, stableRepositoryURL, cif); err != nil {
fmt.Fprintf(out, "WARNING: Failed to download %s: %s (run 'helm repo update')\n", stableRepository, err)
lr, err := initLocalRepo(home.LocalRepository(localRepoIndexFilePath), home.CacheIndex("local"))
if err != nil {
return err
}
f.Add(sr)
f.Add(lr)
if err := f.WriteFile(repoFile, 0644); err != nil {
return err
}
} else if fi.IsDir() {
return fmt.Errorf("%s must be a file, not a directory", repoFile)
}
if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate {
fmt.Fprintln(out, "Updating repository file format...")
if err := r.WriteFile(repoFile, 0644); err != nil {
return err
}
return nil
}
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 fi, err := os.Stat(localRepoIndexFile); err != nil {
fmt.Fprintf(out, "Creating %s \n", localRepoIndexFile)
if err := r.DownloadIndexFile(); err != nil {
return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error())
}
return &c, nil
}
func initLocalRepo(indexFile, cacheFile string) (*repo.Entry, error) {
if fi, err := os.Stat(indexFile); err != nil {
i := repo.NewIndexFile()
if err := i.WriteFile(localRepoIndexFile, 0644); err != nil {
return err
if err := i.WriteFile(indexFile, 0644); err != nil {
return nil, err
}
//TODO: take this out and replace with helm update functionality
os.Symlink(localRepoIndexFile, home.CacheIndex("local"))
os.Symlink(indexFile, cacheFile)
} 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
}

@ -170,7 +170,13 @@ func TestEnsureHome(t *testing.T) {
b := bytes.NewBuffer(nil)
hh := helmpath.Home(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)
}

@ -19,8 +19,6 @@ package main
import (
"fmt"
"io"
"path/filepath"
"strings"
"github.com/spf13/cobra"
@ -32,8 +30,13 @@ type repoAddCmd struct {
name string
url string
home helmpath.Home
out io.Writer
noupdate bool
certFile string
keyFile string
caFile string
out io.Writer
}
func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -56,73 +59,54 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
return add.run()
},
}
f := cmd.Flags()
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
}
func (a *repoAddCmd) run() error {
var err error
if a.noupdate {
err = addRepository(a.name, a.url, a.home)
} else {
err = updateRepository(a.name, a.url, a.home)
}
if err != nil {
if err := addRepository(a.name, a.url, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil {
return err
}
fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name)
return nil
}
func addRepository(name, url string, home helmpath.Home) 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)
func addRepository(name, url string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error {
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
if err != nil {
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)
}
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)
if err := repo.DownloadIndexFile(name, url, cif); err != nil {
return err
c := repo.Entry{
Name: name,
Cache: cif,
URL: url,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
}
return updateRepoLine(name, url, home)
}
func updateRepoLine(name, url string, home helmpath.Home) error {
cif := home.CacheIndex(name)
f, err := repo.LoadRepositoriesFile(home.RepositoryFile())
r, err := repo.NewChartRepository(&c)
if err != nil {
return err
}
f.Update(&repo.Entry{
Name: name,
URL: url,
Cache: filepath.Base(cif),
})
if err := r.DownloadIndexFile(); err != nil {
return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error())
}
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)
}
if err := addRepository(testName, ts.URL(), hh); err != nil {
if err := addRepository(testName, ts.URL(), hh, "", "", "", true); err != nil {
t.Error(err)
}
@ -93,11 +93,11 @@ func TestRepoAdd(t *testing.T) {
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)
}
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")
}
}

@ -24,24 +24,33 @@ import (
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
)
func TestRepoRemove(t *testing.T) {
testURL := "https://test-url.com"
b := bytes.NewBuffer(nil)
home, err := tempHelmHome(t)
ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*")
if err != nil {
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 {
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)
}

@ -36,10 +36,14 @@ Information is cached locally, where it is used by commands like 'helm search'.
future releases.
`
var (
errNoRepositories = errors.New("no repositories found. You must add one before updating")
)
type repoUpdateCmd struct {
update func([]*repo.Entry, bool, io.Writer, helmpath.Home)
out io.Writer
update func([]*repo.ChartRepository, io.Writer)
home helmpath.Home
out io.Writer
}
func newRepoUpdateCmd(out io.Writer) *cobra.Command {
@ -67,31 +71,39 @@ func (u *repoUpdateCmd) run() error {
}
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
}
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...")
var wg sync.WaitGroup
for _, re := range repos {
wg.Add(1)
go func(n, u string) {
go func(re *repo.ChartRepository) {
defer wg.Done()
if n == localRepository {
// We skip local because the indices are symlinked.
if re.Config.Name == localRepository {
fmt.Fprintf(out, "...Skip %s chart repository", re.Config.Name)
return
}
err := repo.DownloadIndexFile(n, u, home.CacheIndex(n))
err := re.DownloadIndexFile()
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 {
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()
fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ")

@ -43,15 +43,15 @@ func TestUpdateCmd(t *testing.T) {
out := bytes.NewBuffer(nil)
// Instead of using the HTTP updater, we provide our own for this test.
// 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 {
fmt.Fprintln(out, re.Name)
fmt.Fprintln(out, re.Config.Name)
}
}
uc := &repoUpdateCmd{
out: out,
update: updater,
home: helmpath.Home(thome),
out: out,
}
if err := uc.run(); err != nil {
t.Fatal(err)
@ -63,33 +63,40 @@ func TestUpdateCmd(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 {
t.Fatal(err)
}
oldhome := homePath()
helmHome = thome
hh := helmpath.Home(thome)
defer func() {
srv.Stop()
ts.Stop()
helmHome = oldhome
os.Remove(thome)
}()
if err := ensureTestHome(helmpath.Home(thome), t); err != nil {
if err := ensureTestHome(hh, t); err != nil {
t.Fatal(err)
}
buf := bytes.NewBuffer(nil)
repos := []*repo.Entry{
{Name: "charts", URL: srv.URL()},
r, err := repo.NewChartRepository(&repo.Entry{
Name: "charts",
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") {
t.Errorf("Failed to get a repo: %q", got)
}
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"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
@ -36,6 +33,7 @@ import (
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/urlutil"
)
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
// This can leave the index in an unsorted state
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 != "" {
var err error
_, file := filepath.Split(filename)
u, err = urlJoin(baseURL, file)
u, err = urlutil.URLJoin(baseURL, file)
if err != nil {
u = filepath.Join(baseURL, file)
}
@ -228,33 +235,10 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
return index, nil
}
// DownloadIndexFile fetches the index from a repository.
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.
// 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.
func LoadIndex(data []byte) (*IndexFile, error) {
func loadIndex(data []byte) (*IndexFile, error) {
i := &IndexFile{}
if err := yaml.Unmarshal(data, i); err != nil {
return i, err
@ -312,30 +296,3 @@ func loadUnversionedIndex(data []byte) (*IndexFile, error) {
}
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 {
t.Fatal(err)
}
i, err := LoadIndex(b)
i, err := loadIndex(b)
if err != nil {
t.Fatal(err)
}
@ -132,21 +132,30 @@ func TestDownloadIndexFile(t *testing.T) {
}
defer os.RemoveAll(dirName)
path := filepath.Join(dirName, testRepo+"-index.yaml")
if err := DownloadIndexFile(testRepo, srv.URL, path); err != nil {
indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml")
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)
}
if _, err := os.Stat(path); err != nil {
if _, err := os.Stat(indexFilePath); err != nil {
t.Errorf("error finding created index file: %#v", err)
}
b, err := ioutil.ReadFile(path)
b, err := ioutil.ReadFile(indexFilePath)
if err != nil {
t.Errorf("error reading index file: %#v", err)
}
i, err := LoadIndex(b)
i, err := loadIndex(b)
if err != nil {
t.Errorf("Index %q failed to parse: %s", testfile, err)
return
@ -305,7 +314,6 @@ func TestLoadUnversionedIndex(t *testing.T) {
}
func TestIndexAdd(t *testing.T) {
i := NewIndexFile()
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])
}
}
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"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"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
// is fixable.
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
type RepoFile struct {
APIVersion string `json:"apiVersion"`
@ -160,83 +140,3 @@ func (r *RepoFile) WriteFile(path string, perm os.FileMode) error {
}
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
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
"k8s.io/helm/pkg/proto/hapi/chart"
)
import "testing"
const testRepositoriesFile = "testdata/repositories.yaml"
const testRepository = "testdata/repository"
const testURL = "http://example-charts.com"
func TestRepoFile(t *testing.T) {
rf := NewRepoFile()
@ -68,7 +58,7 @@ func TestRepoFile(t *testing.T) {
}
}
func TestLoadRepositoriesFile(t *testing.T) {
func TestNewRepositoriesFile(t *testing.T) {
expects := NewRepoFile()
expects.Add(
&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")
if err != nil && err != ErrRepoOutOfDate {
t.Fatal(err)
@ -126,139 +116,3 @@ func TestLoadPreV1RepositoriesFile(t *testing.T) {
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"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
)
@ -69,7 +70,7 @@ func NewServer(docroot string) *Server {
}
srv.start()
// 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)
}
return srv
@ -158,11 +159,13 @@ func (s *Server) LinkIndices() error {
}
// setTestingRepository sets up a testing repository.yaml with only the given name/URL.
func setTestingRepository(helmhome, name, url string) error {
rf := repo.NewRepoFile()
rf.Add(&repo.Entry{Name: name, URL: url})
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
dest := filepath.Join(helmhome, "repository/repositories.yaml")
return rf.WriteFile(dest, 0644)
func setTestingRepository(home helmpath.Home, name, url string) error {
r := repo.NewRepoFile()
r.Add(&repo.Entry{
Name: name,
URL: url,
Cache: home.CacheIndex(name),
})
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