Commit 593718d7 authored by Matt Butcher's avatar Matt Butcher

feat(helm): add 'helm dependency' commands

This also refactors significant portions of the CLI, moving much of the
shared code into a library.

Also in this release, a testing repository server has been added.
parent a5921faf
...@@ -27,6 +27,11 @@ import ( ...@@ -27,6 +27,11 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
) )
const (
reqLock = "requirements.lock"
reqYaml = "requirements.yaml"
)
const dependencyDesc = ` const dependencyDesc = `
Manage the dependencies of a chart. Manage the dependencies of a chart.
...@@ -74,7 +79,7 @@ if it cannot find a requirements.yaml. ...@@ -74,7 +79,7 @@ if it cannot find a requirements.yaml.
func newDependencyCmd(out io.Writer) *cobra.Command { func newDependencyCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dependency update|list", Use: "dependency update|build|list",
Aliases: []string{"dep", "dependencies"}, Aliases: []string{"dep", "dependencies"},
Short: "manage a chart's dependencies", Short: "manage a chart's dependencies",
Long: dependencyDesc, Long: dependencyDesc,
...@@ -82,6 +87,7 @@ func newDependencyCmd(out io.Writer) *cobra.Command { ...@@ -82,6 +87,7 @@ func newDependencyCmd(out io.Writer) *cobra.Command {
cmd.AddCommand(newDependencyListCmd(out)) cmd.AddCommand(newDependencyListCmd(out))
cmd.AddCommand(newDependencyUpdateCmd(out)) cmd.AddCommand(newDependencyUpdateCmd(out))
cmd.AddCommand(newDependencyBuildCmd(out))
return cmd return cmd
} }
...@@ -146,10 +152,14 @@ func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string { ...@@ -146,10 +152,14 @@ func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string {
if err != nil { if err != nil {
return "corrupt" return "corrupt"
} }
if c.Metadata.Name == dep.Name && c.Metadata.Version == dep.Version { if c.Metadata.Name != dep.Name {
return "ok" return "misnamed"
}
if c.Metadata.Version != dep.Version {
return "wrong version"
} }
return "mismatch" return "ok"
} }
folder := filepath.Join(l.chartpath, "charts", dep.Name) folder := filepath.Join(l.chartpath, "charts", dep.Name)
...@@ -196,6 +206,14 @@ func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Wr ...@@ -196,6 +206,14 @@ func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Wr
} }
for _, f := range files { for _, f := range files {
fi, err := os.Stat(f)
if err != nil {
fmt.Fprintf(l.out, "Warning: %s\n", err)
}
// Skip anything that is not a directory and not a tgz file.
if !fi.IsDir() && filepath.Ext(f) != ".tgz" {
continue
}
c, err := chartutil.Load(f) c, err := chartutil.Load(f)
if err != nil { if err != nil {
fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f) fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f)
......
/*
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 main
import (
"io"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
"k8s.io/helm/cmd/helm/helmpath"
)
const dependencyBuildDesc = `
Build out the charts/ directory from the requirements.lock file.
Build is used to reconstruct a chart's dependencies to the state specified in
the lock file. This will not re-negotiate dependencies, as 'helm dependency update'
does.
If no lock file is found, 'helm dependency build' will mirror the behavior
of 'helm dependency update'.
`
type dependencyBuildCmd struct {
out io.Writer
chartpath string
verify bool
keyring string
helmhome helmpath.Home
}
func newDependencyBuildCmd(out io.Writer) *cobra.Command {
dbc := &dependencyBuildCmd{
out: out,
}
cmd := &cobra.Command{
Use: "build [flags] CHART",
Short: "rebuild the charts/ directory based on the requirements.lock file",
Long: dependencyBuildDesc,
RunE: func(cmd *cobra.Command, args []string) error {
dbc.helmhome = helmpath.Home(homePath())
dbc.chartpath = "."
if len(args) > 0 {
dbc.chartpath = args[0]
}
return dbc.run()
},
}
f := cmd.Flags()
f.BoolVar(&dbc.verify, "verify", false, "Verify the packages against signatures.")
f.StringVar(&dbc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
return cmd
}
func (d *dependencyBuildCmd) run() error {
man := &downloader.Manager{
Out: d.out,
ChartPath: d.chartpath,
HelmHome: d.helmhome,
Keyring: d.keyring,
}
if d.verify {
man.Verify = downloader.VerifyIfPossible
}
return man.Build()
}
/*
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 main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
)
func TestDependencyBuildCmd(t *testing.T) {
oldhome := helmHome
hh, err := tempHelmHome()
if err != nil {
t.Fatal(err)
}
helmHome = hh
defer func() {
os.RemoveAll(hh)
helmHome = oldhome
}()
srv := repotest.NewServer(hh)
defer srv.Stop()
_, err = srv.CopyCharts("testdata/testcharts/*.tgz")
if err != nil {
t.Fatal(err)
}
chartname := "depbuild"
if err := createTestingChart(hh, chartname, srv.URL()); err != nil {
t.Fatal(err)
}
out := bytes.NewBuffer(nil)
dbc := &dependencyBuildCmd{out: out}
dbc.helmhome = helmpath.Home(hh)
dbc.chartpath = filepath.Join(hh, chartname)
// In the first pass, we basically want the same results as an update.
if err := dbc.run(); err != nil {
output := out.String()
t.Logf("Output: %s", output)
t.Fatal(err)
}
output := out.String()
if !strings.Contains(output, `update from the "test" chart repository`) {
t.Errorf("Repo did not get updated\n%s", output)
}
// Make sure the actual file got downloaded.
expect := filepath.Join(hh, chartname, "charts/reqtest-0.1.0.tgz")
if _, err := os.Stat(expect); err != nil {
t.Fatal(err)
}
// In the second pass, we want to remove the chart's request dependency,
// then see if it restores from the lock.
lockfile := filepath.Join(hh, chartname, "requirements.lock")
if _, err := os.Stat(lockfile); err != nil {
t.Fatal(err)
}
if err := os.RemoveAll(expect); err != nil {
t.Fatal(err)
}
if err := dbc.run(); err != nil {
output := out.String()
t.Logf("Output: %s", output)
t.Fatal(err)
}
// Now repeat the test that the dependency exists.
expect = filepath.Join(hh, chartname, "charts/reqtest-0.1.0.tgz")
if _, err := os.Stat(expect); err != nil {
t.Fatal(err)
}
// Make sure that build is also fetching the correct version.
hash, err := provenance.DigestFile(expect)
if err != nil {
t.Fatal(err)
}
i, err := repo.LoadIndexFile(cacheIndexFile("test"))
if err != nil {
t.Fatal(err)
}
if h := i.Entries["reqtest-0.1.0"].Digest; h != hash {
t.Errorf("Failed hash match: expected %s, got %s", hash, h)
}
}
...@@ -16,21 +16,12 @@ limitations under the License. ...@@ -16,21 +16,12 @@ limitations under the License.
package main package main
import ( import (
"errors"
"fmt"
"io" "io"
"io/ioutil"
"net/url"
"os"
"path/filepath" "path/filepath"
"strings"
"github.com/ghodss/yaml"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
"k8s.io/helm/cmd/helm/resolver" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/repo"
) )
const dependencyUpDesc = ` const dependencyUpDesc = `
...@@ -38,15 +29,16 @@ Update the on-disk dependencies to mirror the requirements.yaml file. ...@@ -38,15 +29,16 @@ Update the on-disk dependencies to mirror the requirements.yaml file.
This command verifies that the required charts, as expressed in 'requirements.yaml', This command verifies that the required charts, as expressed in 'requirements.yaml',
are present in 'charts/' and are at an acceptable version. are present in 'charts/' and are at an acceptable version.
On successful update, this will generate a lock file that can be used to
rebuild the requirements to an exact version.
` `
// dependencyUpdateCmd describes a 'helm dependency update' // dependencyUpdateCmd describes a 'helm dependency update'
type dependencyUpdateCmd struct { type dependencyUpdateCmd struct {
out io.Writer out io.Writer
chartpath string chartpath string
repoFile string helmhome helmpath.Home
repopath string
helmhome string
verify bool verify bool
keyring string keyring string
} }
...@@ -74,16 +66,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { ...@@ -74,16 +66,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
return err return err
} }
duc.helmhome = homePath() duc.helmhome = helmpath.Home(homePath())
duc.repoFile = repositoriesFile()
duc.repopath = repositoryDirectory()
return duc.run() return duc.run()
}, },
} }
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&duc.verify, "verify", false, "Verify the package against its signature.") f.BoolVar(&duc.verify, "verify", false, "Verify the packages against signatures.")
f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
return cmd return cmd
...@@ -91,197 +81,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { ...@@ -91,197 +81,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
// run runs the full dependency update process. // run runs the full dependency update process.
func (d *dependencyUpdateCmd) run() error { func (d *dependencyUpdateCmd) run() error {
if fi, err := os.Stat(d.chartpath); err != nil { man := &downloader.Manager{
return fmt.Errorf("could not find %s: %s", d.chartpath, err) Out: d.out,
} else if !fi.IsDir() { ChartPath: d.chartpath,
return errors.New("only unpacked charts can be updated") HelmHome: d.helmhome,
} Keyring: d.keyring,
c, err := chartutil.LoadDir(d.chartpath)
if err != nil {
return err
}
req, err := chartutil.LoadRequirements(c)
if err != nil {
if err == chartutil.ErrRequirementsNotFound {
fmt.Fprintf(d.out, "No requirements found in %s/charts.\n", d.chartpath)
return nil
}
return err
}
// For each repo in the file, update the cached copy of that repo
if _, err := d.updateRepos(req.Dependencies); err != nil {
return err
}
// Now we need to find out which version of a chart best satisfies the
// requirements the requirements.yaml
lock, err := d.resolve(req)
if err != nil {
return err
}
// Now we need to fetch every package here into charts/
if err := d.downloadAll(lock.Dependencies); err != nil {
return err
}
// Finally, we need to write the lockfile.
return writeLock(d.chartpath, lock)
}
// resolve takes a list of requirements and translates them into an exact version to download.
//
// This returns a lock file, which has all of the requirements normalized to a specific version.
func (d *dependencyUpdateCmd) resolve(req *chartutil.Requirements) (*chartutil.RequirementsLock, error) {
res := resolver.New(d.chartpath, d.helmhome)
return res.Resolve(req)
}
// downloadAll takes a list of dependencies and downloads them into charts/
func (d *dependencyUpdateCmd) downloadAll(deps []*chartutil.Dependency) error {
repos, err := loadChartRepositories(d.repopath)
if err != nil {
return err
}
fmt.Fprintf(d.out, "Saving %d charts\n", len(deps))
for _, dep := range deps {
fmt.Fprintf(d.out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
target := fmt.Sprintf("%s-%s", dep.Name, dep.Version)
churl, err := findChartURL(target, dep.Repository, repos)
if err != nil {
fmt.Fprintf(d.out, "WARNING: %s (skipped)", err)
continue
}
dest := filepath.Join(d.chartpath, "charts", target+".tgz")
data, err := downloadChart(churl, d.verify, d.keyring)
if err != nil {
fmt.Fprintf(d.out, "WARNING: Could not download %s: %s (skipped)", churl, err)
continue
}
if err := ioutil.WriteFile(dest, data.Bytes(), 0655); err != nil {
fmt.Fprintf(d.out, "WARNING: %s (skipped)", err)
continue
}
}
return nil
}
// updateRepos updates all of the local repos to their latest.
//
// If one of the dependencies present is not in the cached repos, this will error out. The
// consequence of that is that every repository referenced in a requirements.yaml file
// must also be added with 'helm repo add'.
func (d *dependencyUpdateCmd) updateRepos(deps []*chartutil.Dependency) (*repo.RepoFile, error) {
// TODO: In the future, we could make it so that only the repositories that
// are used by this chart are updated. As it is, we're mainly doing some sanity
// checking here.
rf, err := repo.LoadRepositoriesFile(d.repoFile)
if err != nil {
return rf, err
}
repos := rf.Repositories
// Verify that all repositories referenced in the deps are actually known
// by Helm.
missing := []string{}
for _, dd := range deps {
found := false
if dd.Repository == "" {
found = true
} else {
for _, repo := range repos {
if urlsAreEqual(repo, dd.Repository) {
found = true
}
}
}
if !found {
missing = append(missing, dd.Repository)
}
}
if len(missing) > 0 {
return rf, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
} }
if d.verify {
if len(repos) > 0 { man.Verify = downloader.VerifyIfPossible
// This prints errors straight to out.
updateCharts(repos, flagDebug, d.out)
}
return rf, nil
}
// urlsAreEqual normalizes two URLs and then compares for equality.
func urlsAreEqual(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
return a == b
}
bu, err := url.Parse(b)
if err != nil {
return false
}
return au.String() == bu.String()
}
// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified.
//
// In this current version, name is of the form 'foo-1.2.3'. This will change when
// the repository index stucture changes.
func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos {
if urlsAreEqual(repourl, cr.URL) {
for ename, entry := range cr.IndexFile.Entries {
if ename == name {
return entry.URL, nil
}
}
}
}
return "", fmt.Errorf("chart %s not found in %s", name, repourl)
}
// loadChartRepositories reads the repositories.yaml, and then builds a map of
// ChartRepositories.
//
// The key is the local name (which is only present in the repositories.yaml).
func loadChartRepositories(repodir string) (map[string]*repo.ChartRepository, error) {
indices := map[string]*repo.ChartRepository{}
repoyaml := repositoriesFile()
// Load repositories.yaml file
rf, err := repo.LoadRepositoriesFile(repoyaml)
if err != nil {
return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
}
// localName: chartRepo
for lname, url := range rf.Repositories {
index, err := repo.LoadIndexFile(cacheIndexFile(lname))
if err != nil {
return indices, err
}
cr := &repo.ChartRepository{
URL: url,
IndexFile: index,
}
indices[lname] = cr
}
return indices, nil
}
// writeLock writes a lockfile to disk
func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
data, err := yaml.Marshal(lock)
if err != nil {
return err
} }
dest := filepath.Join(chartpath, "requirements.lock") return man.Update()
return ioutil.WriteFile(dest, data, 0755)
} }
...@@ -18,8 +18,6 @@ package main ...@@ -18,8 +18,6 @@ package main
import ( import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
...@@ -27,10 +25,12 @@ import ( ...@@ -27,10 +25,12 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/cmd/helm/helmpath"
"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/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest"
) )
func TestDependencyUpdateCmd(t *testing.T) { func TestDependencyUpdateCmd(t *testing.T) {
...@@ -40,36 +40,35 @@ func TestDependencyUpdateCmd(t *testing.T) { ...@@ -40,36 +40,35 @@ func TestDependencyUpdateCmd(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
helmHome = hh // Shoot me now. helmHome = hh
defer func() { defer func() {
os.RemoveAll(hh) os.RemoveAll(hh)
helmHome = oldhome helmHome = oldhome
}() }()
srv := newTestingRepositoryServer(hh) srv := repotest.NewServer(hh)
defer srv.stop() defer srv.Stop()
copied, err := srv.copyCharts("testdata/testcharts/*.tgz") copied, err := srv.CopyCharts("testdata/testcharts/*.tgz")
t.Logf("Copied charts %s", strings.Join(copied, "\n")) t.Logf("Copied charts:\n%s", strings.Join(copied, "\n"))
t.Logf("Listening for directory %s", srv.docroot) t.Logf("Listening on directory %s", srv.Root())
chartname := "depup" chartname := "depup"
if err := createTestingChart(hh, chartname, srv.url()); err != nil { if err := createTestingChart(hh, chartname, srv.URL()); err != nil {
t.Fatal(err) t.Fatal(err)
} }
out := bytes.NewBuffer(nil) out := bytes.NewBuffer(nil)
duc := &dependencyUpdateCmd{out: out} duc := &dependencyUpdateCmd{out: out}
duc.helmhome = hh duc.helmhome = helmpath.Home(hh)
duc.chartpath = filepath.Join(hh, chartname) duc.chartpath = filepath.Join(hh, chartname)
duc.repoFile = filepath.Join(duc.helmhome, "repository/repositories.yaml")
duc.repopath = filepath.Join(duc.helmhome, "repository")
if err := duc.run(); err != nil { if err := duc.run(); err != nil {
output := out.String()
t.Logf("Output: %s", output)
t.Fatal(err) t.Fatal(err)
} }
output := out.String() output := out.String()
t.Logf("Output: %s", output)
// This is written directly to stdout, so we have to capture as is. // This is written directly to stdout, so we have to capture as is.
if !strings.Contains(output, `update from the "test" chart repository`) { if !strings.Contains(output, `update from the "test" chart repository`) {
t.Errorf("Repo did not get updated\n%s", output) t.Errorf("Repo did not get updated\n%s", output)
...@@ -98,95 +97,6 @@ func TestDependencyUpdateCmd(t *testing.T) { ...@@ -98,95 +97,6 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Logf("Results: %s", out.String()) t.Logf("Results: %s", out.String())
} }
// newTestingRepositoryServer creates a repository server for testing.
//
// docroot should be a temp dir managed by the caller.
//
// This will start the server, serving files off of the docroot.
//
// Use copyCharts to move charts into the repository and then index them
// for service.
func newTestingRepositoryServer(docroot string) *testingRepositoryServer {
root, err := filepath.Abs(docroot)
if err != nil {
panic(err)
}
srv := &testingRepositoryServer{
docroot: root,
}
srv.start()
// Add the testing repository as the only repo.
if err := setTestingRepository(docroot, "test", srv.url()); err != nil {
panic(err)
}
return srv
}
type testingRepositoryServer struct {
docroot string
srv *httptest.Server
}
// copyCharts takes a glob expression and copies those charts to the server root.
func (s *testingRepositoryServer) copyCharts(origin string) ([]string, error) {
files, err := filepath.Glob(origin)
if err != nil {
return []string{}, err
}
copied := make([]string, len(files))
for i, f := range files {
base := filepath.Base(f)
newname := filepath.Join(s.docroot, base)
data, err := ioutil.ReadFile(f)
if err != nil {
return []string{}, err
}
if err := ioutil.WriteFile(newname, data, 0755); err != nil {
return []string{}, err
}
copied[i] = newname
}
// generate the index
index, err := repo.IndexDirectory(s.docroot, s.url())
if err != nil {
return copied, err
}
d, err := yaml.Marshal(index.Entries)
if err != nil {
return copied, err
}
ifile := filepath.Join(s.docroot, "index.yaml")
err = ioutil.WriteFile(ifile, d, 0755)
return copied, err
}
func (s *testingRepositoryServer) start() {
s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot)))
}
func (s *testingRepositoryServer) stop() {
s.srv.Close()
}
func (s *testingRepositoryServer) url() string {
return s.srv.URL
}
// setTestingRepository sets up a testing repository.yaml with only the given name/URL.
func setTestingRepository(helmhome, name, url string) error {
// Oddly, there is no repo.Save function for this.
data, err := yaml.Marshal(&map[string]string{name: url})
if err != nil {
return err
}
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
dest := filepath.Join(helmhome, "repository/repositories.yaml")
return ioutil.WriteFile(dest, data, 0666)
}
// createTestingChart creates a basic chart that depends on reqtest-0.1.0 // createTestingChart creates a basic chart that depends on reqtest-0.1.0
// //
// The baseURL can be used to point to a particular repository server. // The baseURL can be used to point to a particular repository server.
......
/*
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 downloader
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo"
)
// VerificationStrategy describes a strategy for determining whether to verify a chart.
type VerificationStrategy int
const (
// VerifyNever will skip all verification of a chart.
VerifyNever VerificationStrategy = iota
// VerifyIfPossible will attempt a verification, it will not error if verification
// data is missing. But it will not stop processing if verification fails.
VerifyIfPossible
// VerifyAlways will always attempt a verification, and will fail if the
// verification fails.
VerifyAlways
)
// ChartDownloader handles downloading a chart.
//
// It is capable of performing verifications on charts as well.
type ChartDownloader struct {
// Out is the location to write warning and info messages.
Out io.Writer
// Verify indicates what verification strategy to use.
Verify VerificationStrategy
// Keyring is the keyring file used for verification.
Keyring string
// HelmHome is the $HELM_HOME.
HelmHome helmpath.Home
}
// DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file.
//
// If Verify is set to VerifyNever, the verification will be nil.
// If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure.
// If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails.
//
// For VerifyNever and VerifyIfPossible, the Verification may be empty.
func (c *ChartDownloader) DownloadTo(ref string, dest string) (*provenance.Verification, error) {
// resolve URL
u, err := c.ResolveChartRef(ref)
if err != nil {
return nil, err
}
data, err := download(u.String())
if err != nil {
return nil, err
}
name := filepath.Base(u.Path)
destfile := filepath.Join(dest, name)
if err := ioutil.WriteFile(destfile, data.Bytes(), 0655); err != nil {
return nil, err
}
// If provenance is requested, verify it.
ver := &provenance.Verification{}
if c.Verify > VerifyNever {
body, err := download(u.String() + ".prov")
if err != nil {
if c.Verify == VerifyAlways {
return ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov")
}
fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err)
return ver, nil
}
provfile := destfile + ".prov"
if err := ioutil.WriteFile(provfile, body.Bytes(), 0655); err != nil {
return nil, err
}
ver, err = VerifyChart(destfile, c.Keyring)
if err != nil {
// Fail always in this case, since it means the verification step
// failed.
return ver, err
}
}
return ver, nil
}
// ResolveChartRef resolves a chart reference to a URL.
//
// A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
func (c *ChartDownloader) ResolveChartRef(ref string) (*url.URL, error) {
// See if it's already a full URL.
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)
}
r, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
if err != nil {
return u, err
}
// See if it's of the form: repo/path_to_chart
p := strings.Split(ref, "/")
if len(p) > 1 {
if baseURL, ok := r.Repositories[p[0]]; ok {
if !strings.HasSuffix(baseURL, "/") {
baseURL = baseURL + "/"
}
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/"))
}
return u, fmt.Errorf("No such repo: %s", p[0])
}
return u, fmt.Errorf("Invalid chart url format: %s", ref)
}
// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart.
//
// It assumes that a chart archive file is accompanied by a provenance file whose
// name is the archive file name plus the ".prov" extension.
func VerifyChart(path string, keyring string) (*provenance.Verification, error) {
// For now, error out if it's not a tar file.
if fi, err := os.Stat(path); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("unpacked charts cannot be verified")
} else if !isTar(path) {
return nil, errors.New("chart must be a tgz file")
}
provfile := path + ".prov"
if _, err := os.Stat(provfile); err != nil {
return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err)
}
sig, err := provenance.NewFromKeyring(keyring, "")
if err != nil {
return nil, fmt.Errorf("failed to load keyring: %s", err)
}
return sig.Verify(path, provfile)
}
// download performs a simple HTTP Get and returns the body.
func download(href string) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
resp, err := http.Get(href)
if err != nil {
return buf, err
}
if resp.StatusCode != 200 {
return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
}
_, err = io.Copy(buf, resp.Body)
resp.Body.Close()
return buf, err
}
// isTar tests whether the given file is a tar file.
//
// Currently, this simply checks extension, since a subsequent function will
// untar the file and validate its binary format.
func isTar(filename string) bool {
return strings.ToLower(filepath.Ext(filename)) == ".tgz"
}
/*
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 downloader
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo/repotest"
)
func TestResolveChartRef(t *testing.T) {
tests := []struct {
name, ref, expect string
fail bool
}{
{name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"},
{name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"},
{name: "reference, testing repo", ref: "testing/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"},
{name: "full URL, 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},
}
c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"),
Out: os.Stderr,
}
for _, tt := range tests {
u, err := c.ResolveChartRef(tt.ref)
if err != nil {
if tt.fail {
continue
}
t.Errorf("%s: failed with error %s", tt.name, err)
continue
}
if got := u.String(); got != tt.expect {
t.Errorf("%s: expected %s, got %s", tt.name, tt.expect, got)
}
}
}
func TestVerifyChart(t *testing.T) {
v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub")
if err != nil {
t.Fatal(err)
}
// The verification is tested at length in the provenance package. Here,
// we just want a quick sanity check that the v is not empty.
if len(v.FileHash) == 0 {
t.Error("Digest missing")
}
}
func TestDownload(t *testing.T) {
expect := "Call me Ishmael"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, expect)
}))
defer srv.Close()
got, err := download(srv.URL)
if err != nil {
t.Fatal(err)
}
if got.String() != expect {
t.Errorf("Expected %q, got %q", expect, got.String())
}
}
func TestIsTar(t *testing.T) {
tests := map[string]bool{
"foo.tgz": true,
"foo/bar/baz.tgz": true,
"foo-1.2.3.4.5.tgz": true,
"foo.tar.gz": false, // for our purposes
"foo.tgz.1": false,
"footgz": false,
}
for src, expect := range tests {
if isTar(src) != expect {
t.Errorf("%q should be %t", src, expect)
}
}
}
func TestDownloadTo(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-downloadto-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(hh)
dest := filepath.Join(hh, "dest")
os.MkdirAll(dest, 0755)
// Set up a fake repo
srv := repotest.NewServer(hh)
defer srv.Stop()
if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil {
t.Error(err)
return
}
c := ChartDownloader{
HelmHome: helmpath.Home("testdata/helmhome"),
Out: os.Stderr,
Verify: VerifyAlways,
Keyring: "testdata/helm-test-key.pub",
}
cname := "/signtest-0.1.0.tgz"
v, err := c.DownloadTo(srv.URL()+cname, dest)
if err != nil {
t.Error(err)
return
}
if v.FileHash == "" {
t.Error("File hash was empty, but verification is required.")
}
if _, err := os.Stat(filepath.Join(dest, cname)); err != nil {
t.Error(err)
return
}
}
/*
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 downloader provides a library for downloading charts.
This package contains various tools for downloading charts from repository
servers, and then storing them in Helm-specific directory structures (like
HELM_HOME). This library contains many functions that depend on a specific
filesystem layout.
*/
package downloader
/*
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 downloader
import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"github.com/ghodss/yaml"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/cmd/helm/resolver"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/repo"
)
// Manager handles the lifecycle of fetching, resolving, and storing dependencies.
type Manager struct {
// Out is used to print warnings and notifications.
Out io.Writer
// ChartPath is the path to the unpacked base chart upon which this operates.
ChartPath string
// HelmHome is the $HELM_HOME directory
HelmHome helmpath.Home
// Verification indicates whether the chart should be verified.
Verify VerificationStrategy
// Keyring is the key ring file.
Keyring string
}
// Build rebuilds a local charts directory from a lockfile.
//
// If the lockfile is not present, this will run a Manager.Update()
func (m *Manager) Build() error {
c, err := m.loadChartDir()
if err != nil {
return err
}
// If a lock file is found, run a build from that. Otherwise, just do
// an update.
lock, err := chartutil.LoadRequirementsLock(c)
if err != nil {
return m.Update()
}
// A lock must accompany a requirements.yaml file.
req, err := chartutil.LoadRequirements(c)
if err != nil {
return fmt.Errorf("requirements.yaml cannot be opened: %s", err)
}
if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest {
return fmt.Errorf("requirements.lock is out of sync with requirements.yaml")
}
// Check that all of the repos we're dependent on actually exist.
if err := m.hasAllRepos(lock.Dependencies); err != nil {
return err
}
// For each repo in the file, update the cached copy of that repo
if err := m.UpdateRepositories(); err != nil {
return err
}
// Now we need to fetch every package here into charts/
if err := m.downloadAll(lock.Dependencies); err != nil {
return err
}
return nil
}
// Update updates a local charts directory.
//
// It first reads the requirements.yaml file, and then attempts to
// negotiate versions based on that. It will download the versions
// from remote chart repositories.
func (m *Manager) Update() error {
c, err := m.loadChartDir()
if err != nil {
return err
}
// If no requirements file is found, we consider this a successful
// completion.
req, err := chartutil.LoadRequirements(c)
if err != nil {
if err == chartutil.ErrRequirementsNotFound {
fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath)
return nil
}
return err
}
// Check that all of the repos we're dependent on actually exist.
if err := m.hasAllRepos(req.Dependencies); err != nil {
return err
}
// For each repo in the file, update the cached copy of that repo
if err := m.UpdateRepositories(); err != nil {
return err
}
// Now we need to find out which version of a chart best satisfies the
// requirements the requirements.yaml
lock, err := m.resolve(req)
if err != nil {
return err
}
// Now we need to fetch every package here into charts/
if err := m.downloadAll(lock.Dependencies); err != nil {
return err
}
// Finally, we need to write the lockfile.
return writeLock(m.ChartPath, lock)
}
func (m *Manager) loadChartDir() (*chart.Chart, error) {
if fi, err := os.Stat(m.ChartPath); err != nil {
return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err)
} else if !fi.IsDir() {
return nil, errors.New("only unpacked charts can be updated")
}
return chartutil.LoadDir(m.ChartPath)
}
// resolve takes a list of requirements and translates them into an exact version to download.
//
// This returns a lock file, which has all of the requirements normalized to a specific version.
func (m *Manager) resolve(req *chartutil.Requirements) (*chartutil.RequirementsLock, error) {
res := resolver.New(m.ChartPath, m.HelmHome)
return res.Resolve(req)
}
// downloadAll takes a list of dependencies and downloads them into charts/
func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
repos, err := m.loadChartRepositories()
if err != nil {
return err
}
dl := ChartDownloader{
Out: m.Out,
Verify: m.Verify,
Keyring: m.Keyring,
HelmHome: m.HelmHome,
}
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
for _, dep := range deps {
fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
target := fmt.Sprintf("%s-%s", dep.Name, dep.Version)
churl, err := findChartURL(target, dep.Repository, repos)
if err != nil {
fmt.Fprintf(m.Out, "WARNING: %s (skipped)", err)
continue
}
dest := filepath.Join(m.ChartPath, "charts")
if _, err := dl.DownloadTo(churl, dest); err != nil {
fmt.Fprintf(m.Out, "WARNING: Could not download %s: %s (skipped)", churl, err)
continue
}
}
return nil
}
// hasAllRepos ensures that all of the referenced deps are in the local repo cache.
func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
if err != nil {
return err
}
repos := rf.Repositories
// Verify that all repositories referenced in the deps are actually known
// by Helm.
missing := []string{}
for _, dd := range deps {
found := false
if dd.Repository == "" {
found = true
} else {
for _, repo := range repos {
if urlsAreEqual(repo, dd.Repository) {
found = true
}
}
}
if !found {
missing = append(missing, dd.Repository)
}
}
if len(missing) > 0 {
return fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
}
return nil
}
// UpdateRepositories updates all of the local repos to the latest.
func (m *Manager) UpdateRepositories() error {
rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())
if err != nil {
return err
}
repos := rf.Repositories
if len(repos) > 0 {
// This prints warnings straight to out.
m.parallelRepoUpdate(repos)
}
return nil
}
func (m *Manager) parallelRepoUpdate(repos map[string]string) {
out := m.Out
fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
var wg sync.WaitGroup
for name, url := range repos {
wg.Add(1)
go func(n, u string) {
err := repo.DownloadIndexFile(n, u, m.HelmHome.CacheIndex(n))
if err != nil {
updateErr := fmt.Sprintf("...Unable to get an update from the %q chart repository: %s", n, err)
fmt.Fprintln(out, updateErr)
} else {
fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", n)
}
wg.Done()
}(name, url)
}
wg.Wait()
fmt.Fprintln(out, "Update Complete. Happy Helming!")
}
// urlsAreEqual normalizes two URLs and then compares for equality.
func urlsAreEqual(a, b string) bool {
au, err := url.Parse(a)
if err != nil {
// 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
}
return au.String() == bu.String()
}
// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified.
//
// In this current version, name is of the form 'foo-1.2.3'. This will change when
// the repository index stucture changes.
func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) {
for _, cr := range repos {
if urlsAreEqual(repourl, cr.URL) {
for ename, entry := range cr.IndexFile.Entries {
if ename == name {
return entry.URL, nil
}
}
}
}
return "", fmt.Errorf("chart %s not found in %s", name, repourl)
}
// loadChartRepositories reads the repositories.yaml, and then builds a map of
// ChartRepositories.
//
// The key is the local name (which is only present in the repositories.yaml).
func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) {
indices := map[string]*repo.ChartRepository{}
repoyaml := m.HelmHome.RepositoryFile()
// Load repositories.yaml file
rf, err := repo.LoadRepositoriesFile(repoyaml)
if err != nil {
return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
}
// localName: chartRepo
for lname, url := range rf.Repositories {
cacheindex := m.HelmHome.CacheIndex(lname)
index, err := repo.LoadIndexFile(cacheindex)
if err != nil {
return indices, err
}
cr := &repo.ChartRepository{
URL: url,
IndexFile: index,
}
indices[lname] = cr
}
return indices, nil
}
// writeLock writes a lockfile to disk
func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
data, err := yaml.Marshal(lock)
if err != nil {
return err
}
dest := filepath.Join(chartpath, "requirements.lock")
return ioutil.WriteFile(dest, data, 0644)
}
alpine-0.1.0:
name: alpine
url: http://storage.googleapis.com/kubernetes-charts/alpine-0.1.0.tgz
created: 2016-09-06 21:58:44.211261566 +0000 UTC
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
chartfile:
name: alpine
home: https://k8s.io/helm
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
engine: ""
icon: ""
mariadb-0.3.0:
name: mariadb
url: http://storage.googleapis.com/kubernetes-charts/mariadb-0.3.0.tgz
created: 2016-09-06 21:58:44.211870222 +0000 UTC
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
chartfile:
name: mariadb
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
engine: gotpl
icon: ""
repository/local/index.yaml
\ No newline at end of file
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0
...
files:
signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b
-----BEGIN PGP SIGNATURE-----
wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g
l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki
DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp
flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz
9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0
S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s=
=NyOM
-----END PGP SIGNATURE-----
\ No newline at end of file
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
.git
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0
description: Deploy a basic Alpine Linux pod
home: https://k8s.io/helm
name: alpine
sources:
- https://github.com/kubernetes/helm
version: 0.1.0
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install docs/examples/alpine`.
apiVersion: v1
kind: Pod
metadata:
name: {{.Release.Name}}-{{.Chart.Name}}
labels:
heritage: {{.Release.Service}}
chartName: {{.Chart.Name}}
chartVersion: {{.Chart.Version | quote}}
annotations:
"helm.sh/created": "{{.Release.Time.Seconds}}"
spec:
restartPolicy: {{default "Never" .restart_policy}}
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]
apiVersion: v1
kind: Pod
metadata:
name: signtest
spec:
restartPolicy: Never
containers:
- name: waiter
image: "alpine:3.3"
command: ["/bin/sleep","9000"]
...@@ -17,21 +17,16 @@ limitations under the License. ...@@ -17,21 +17,16 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo"
) )
const fetchDesc = ` const fetchDesc = `
...@@ -53,6 +48,7 @@ type fetchCmd struct { ...@@ -53,6 +48,7 @@ type fetchCmd struct {
untar bool untar bool
untardir string untardir string
chartRef string chartRef string
destdir string
verify bool verify bool
keyring string keyring string
...@@ -83,9 +79,10 @@ func newFetchCmd(out io.Writer) *cobra.Command { ...@@ -83,9 +79,10 @@ func newFetchCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.") f.BoolVar(&fch.untar, "untar", false, "If set to true, will untar the chart after downloading it.")
f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies where to untar the chart.") f.StringVar(&fch.untardir, "untardir", ".", "If untar is specified, this flag specifies the name of the directory into which the chart is expanded.")
f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.") f.BoolVar(&fch.verify, "verify", false, "Verify the package against its signature.")
f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.") f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
f.StringVarP(&fch.destdir, "destination", "d", ".", "The location to write the chart. If this and tardir are specified, tardir is appended to this.")
return cmd return cmd
} }
...@@ -96,162 +93,60 @@ func (f *fetchCmd) run() error { ...@@ -96,162 +93,60 @@ func (f *fetchCmd) run() error {
pname += ".tgz" pname += ".tgz"
} }
return downloadAndSaveChart(pname, f.untar, f.untardir, f.verify, f.keyring) c := downloader.ChartDownloader{
} HelmHome: helmpath.Home(homePath()),
Out: f.out,
// downloadAndSaveChart fetches a chart over HTTP, and then (if verify is true) verifies it. Keyring: f.keyring,
// Verify: downloader.VerifyNever,
// If untar is true, it also unpacks the file into untardir.
func downloadAndSaveChart(pname string, untar bool, untardir string, verify bool, keyring string) error {
buf, err := downloadChart(pname, verify, keyring)
if err != nil {
return err
} }
return saveChart(pname, buf, untar, untardir)
}
func downloadChart(pname string, verify bool, keyring string) (*bytes.Buffer, error) { if f.verify {
r, err := repo.LoadRepositoriesFile(repositoriesFile()) c.Verify = downloader.VerifyAlways
if err != nil {
return bytes.NewBuffer(nil), err
} }
// get download url // If untar is set, we fetch to a tempdir, then untar and copy after
u, err := mapRepoArg(pname, r.Repositories) // verification.
dest := f.destdir
if f.untar {
var err error
dest, err = ioutil.TempDir("", "helm-")
if err != nil { if err != nil {
return bytes.NewBuffer(nil), err return fmt.Errorf("Failed to untar: %s", err)
} }
defer os.RemoveAll(dest)
href := u.String()
buf, err := fetchChart(href)
if err != nil {
return buf, err
} }
if verify { v, err := c.DownloadTo(pname, dest)
basename := filepath.Base(pname)
sigref := href + ".prov"
sig, err := fetchChart(sigref)
if err != nil { if err != nil {
return buf, fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err)
}
if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil {
return buf, fmt.Errorf("provenance data not saved: %s", err)
}
if err := verifyChart(basename, keyring); err != nil {
return buf, err
}
}
return buf, nil
}
// verifyChart takes a path to a chart archive and a keyring, and verifies the chart.
//
// It assumes that a chart archive file is accompanied by a provenance file whose
// name is the archive file name plus the ".prov" extension.
func verifyChart(path string, keyring string) error {
// For now, error out if it's not a tar file.
if fi, err := os.Stat(path); err != nil {
return err return err
} else if fi.IsDir() {
return errors.New("unpacked charts cannot be verified")
} else if !isTar(path) {
return errors.New("chart must be a tgz file")
} }
provfile := path + ".prov" if f.verify {
if _, err := os.Stat(provfile); err != nil { fmt.Fprintf(f.out, "Verification: %v", v)
return fmt.Errorf("could not load provenance file %s: %s", provfile, err)
} }
sig, err := provenance.NewFromKeyring(keyring, "") // After verification, untar the chart into the requested directory.
if err != nil { if f.untar {
return fmt.Errorf("failed to load keyring: %s", err) ud := f.untardir
} if !filepath.IsAbs(ud) {
ver, err := sig.Verify(path, provfile) ud = filepath.Join(f.destdir, ud)
if flagDebug {
for name := range ver.SignedBy.Identities {
fmt.Printf("Signed by %q\n", name)
}
} }
return err if fi, err := os.Stat(ud); err != nil {
} if err := os.MkdirAll(ud, 0755); err != nil {
return fmt.Errorf("Failed to untar (mkdir): %s", err)
// defaultKeyring returns the expanded path to the default keyring.
func defaultKeyring() string {
return os.ExpandEnv("$HOME/.gnupg/pubring.gpg")
}
// isTar tests whether the given file is a tar file.
//
// Currently, this simply checks extension, since a subsequent function will
// untar the file and validate its binary format.
func isTar(filename string) bool {
return strings.ToLower(filepath.Ext(filename)) == ".tgz"
}
// saveChart saves a chart locally.
func saveChart(name string, buf *bytes.Buffer, untar bool, untardir string) error {
if untar {
return chartutil.Expand(untardir, buf)
} }
p := strings.Split(name, "/") } else if !fi.IsDir() {
return saveChartFile(p[len(p)-1], buf) return fmt.Errorf("Failed to untar: %s is not a directory", ud)
}
// fetchChart retrieves a chart over HTTP.
func fetchChart(href string) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
resp, err := http.Get(href)
if err != nil {
return buf, err
}
if resp.StatusCode != 200 {
return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status)
} }
_, err = io.Copy(buf, resp.Body) from := filepath.Join(dest, filepath.Base(pname))
resp.Body.Close() return chartutil.ExpandFile(ud, from)
return buf, err
}
// mapRepoArg figures out which format the argument is given, and creates a fetchable
// url from it.
func mapRepoArg(arg string, r map[string]string) (*url.URL, error) {
// See if it's already a full URL.
u, err := url.ParseRequestURI(arg)
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 nil, fmt.Errorf("Invalid chart url format: %s", arg)
} }
// See if it's of the form: repo/path_to_chart return nil
p := strings.Split(arg, "/")
if len(p) > 1 {
if baseURL, ok := r[p[0]]; ok {
if !strings.HasSuffix(baseURL, "/") {
baseURL = baseURL + "/"
}
return url.ParseRequestURI(baseURL + strings.Join(p[1:], "/"))
}
return nil, fmt.Errorf("No such repo: %s", p[0])
}
return nil, fmt.Errorf("Invalid chart url format: %s", arg)
} }
func saveChartFile(c string, r io.Reader) error { // defaultKeyring returns the expanded path to the default keyring.
// Grab the chart name that we'll use for the name of the file to download to. func defaultKeyring() string {
out, err := os.Create(c) return os.ExpandEnv("$HOME/.gnupg/pubring.gpg")
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
} }
...@@ -17,49 +17,109 @@ limitations under the License. ...@@ -17,49 +17,109 @@ limitations under the License.
package main package main
import ( import (
"fmt" "bytes"
"os"
"path/filepath"
"testing" "testing"
"k8s.io/helm/pkg/repo/repotest"
) )
type testCase struct { func TestFetchCmd(t *testing.T) {
in string hh, err := tempHelmHome()
expectedErr error if err != nil {
expectedOut string t.Fatal(err)
} }
old := homePath()
helmHome = hh
defer func() {
helmHome = old
os.RemoveAll(hh)
}()
var repos = map[string]string{ // all flags will get "--home=TMDIR -d outdir" appended.
"local": "http://localhost:8879/charts", tests := []struct {
"someother": "http://storage.googleapis.com/mycharts", name string
} chart string
flags []string
fail bool
failExpect string
expectFile string
expectDir bool
}{
{
name: "Basic chart fetch",
chart: "test/signtest-0.1.0",
expectFile: "./signtest-0.1.0.tgz",
},
{
name: "Fail fetching non-existent chart",
chart: "test/nosuchthing-0.1.0",
failExpect: "Failed to fetch",
fail: true,
},
{
name: "Fetch and verify",
chart: "test/signtest-0.1.0",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"},
expectFile: "./signtest-0.1.0.tgz",
},
{
name: "Fetch and fail verify",
chart: "test/reqtest-0.1.0",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"},
failExpect: "Failed to fetch provenance",
fail: true,
},
{
name: "Fetch and untar",
chart: "test/signtest-0.1.0",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"},
expectFile: "./signtest",
expectDir: true,
},
{
name: "Fetch, verify, untar",
chart: "test/signtest-0.1.0",
flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"},
expectFile: "./signtest",
expectDir: true,
},
}
var testCases = []testCase{ srv := repotest.NewServer(hh)
{"bad", fmt.Errorf("Invalid chart url format: bad"), ""}, defer srv.Stop()
{"http://", fmt.Errorf("Invalid chart url format: http://"), ""},
{"http://example.com", fmt.Errorf("Invalid chart url format: http://example.com"), ""},
{"http://example.com/foo/bar", nil, "http://example.com/foo/bar"},
{"local/nginx-2.0.0.tgz", nil, "http://localhost:8879/charts/nginx-2.0.0.tgz"},
{"nonexistentrepo/nginx-2.0.0.tgz", fmt.Errorf("No such repo: nonexistentrepo"), ""},
}
func testRunner(t *testing.T, tc testCase) { if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil {
u, err := mapRepoArg(tc.in, repos) t.Fatal(err)
if (tc.expectedErr == nil && err != nil) ||
(tc.expectedErr != nil && err == nil) ||
(tc.expectedErr != nil && err != nil && tc.expectedErr.Error() != err.Error()) {
t.Errorf("Expected mapRepoArg to fail with input %s %v but got %v", tc.in, tc.expectedErr, err)
} }
if (u == nil && len(tc.expectedOut) != 0) || t.Logf("HELM_HOME=%s", homePath())
(u != nil && len(tc.expectedOut) == 0) ||
(u != nil && tc.expectedOut != u.String()) {
t.Errorf("Expected %s to map to fetch url %v but got %v", tc.in, tc.expectedOut, u)
}
} for _, tt := range tests {
outdir := filepath.Join(hh, "testout")
os.RemoveAll(outdir)
os.Mkdir(outdir, 0755)
func TestMappings(t *testing.T) { buf := bytes.NewBuffer(nil)
for _, tc := range testCases { cmd := newFetchCmd(buf)
testRunner(t, tc) tt.flags = append(tt.flags, "-d", outdir)
cmd.ParseFlags(tt.flags)
if err := cmd.RunE(cmd, []string{tt.chart}); err != nil {
if tt.fail {
continue
}
t.Errorf("%q reported error: %s", tt.name, err)
continue
}
ef := filepath.Join(outdir, tt.expectFile)
fi, err := os.Stat(ef)
if err != nil {
t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err)
}
if fi.IsDir() != tt.expectDir {
t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir)
}
} }
} }
/*
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 helmpath
import (
"fmt"
"path/filepath"
)
// Home describes the location of a CLI configuration.
//
// This helper builds paths relative to a Helm Home directory.
type Home string
// String returns Home as a string.
//
// Implements fmt.Stringer.
func (h Home) String() string {
return string(h)
}
// Repository returns the path to the local repository.
func (h Home) Repository() string {
return filepath.Join(string(h), "repository")
}
// RepositoryFile returns the path to the repositories.yaml file.
func (h Home) RepositoryFile() string {
return filepath.Join(string(h), "repository/repositories.yaml")
}
// Cache returns the path to the local cache.
func (h Home) Cache() string {
return filepath.Join(string(h), "repository/cache")
}
// CacheIndex returns the path to an index for the given named repository.
func (h Home) CacheIndex(name string) string {
target := fmt.Sprintf("repository/cache/%s-index.yaml", name)
return filepath.Join(string(h), target)
}
// LocalRepository returns the location to the local repo.
//
// The local repo is the one used by 'helm serve'
func (h Home) LocalRepository() string {
return filepath.Join(string(h), "repository/local")
}
/*
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 helmpath
import (
"testing"
)
func TestHelmHome(t *testing.T) {
hh := Home("/r")
isEq := func(t *testing.T, a, b string) {
if a != b {
t.Errorf("Expected %q, got %q", a, b)
}
}
isEq(t, hh.String(), "/r")
isEq(t, hh.Repository(), "/r/repository")
isEq(t, hh.RepositoryFile(), "/r/repository/repositories.yaml")
isEq(t, hh.LocalRepository(), "/r/repository/local")
isEq(t, hh.Cache(), "/r/repository/cache")
isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml")
}
...@@ -32,6 +32,8 @@ import ( ...@@ -32,6 +32,8 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
...@@ -286,7 +288,7 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { ...@@ -286,7 +288,7 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
if fi.IsDir() { if fi.IsDir() {
return "", errors.New("cannot verify a directory") return "", errors.New("cannot verify a directory")
} }
if err := verifyChart(abs, keyring); err != nil { if _, err := downloader.VerifyChart(abs, keyring); err != nil {
return "", err return "", err
} }
} }
...@@ -306,7 +308,17 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) { ...@@ -306,7 +308,17 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
if filepath.Ext(name) != ".tgz" { if filepath.Ext(name) != ".tgz" {
name += ".tgz" name += ".tgz"
} }
if err := downloadAndSaveChart(name, false, ".", verify, keyring); err == nil {
dl := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()),
Out: os.Stdout,
Keyring: keyring,
}
if verify {
dl.Verify = downloader.VerifyAlways
}
if _, err := dl.DownloadTo(name, "."); err == nil {
lname, err := filepath.Abs(filepath.Base(name)) lname, err := filepath.Abs(filepath.Base(name))
if err != nil { if err != nil {
return lname, err return lname, err
......
...@@ -34,7 +34,7 @@ var testName = "test-name" ...@@ -34,7 +34,7 @@ var testName = "test-name"
func TestRepoAddCmd(t *testing.T) { func TestRepoAddCmd(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "OK") fmt.Fprintln(w, "")
})) }))
tests := []releaseCase{ tests := []releaseCase{
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/provenance"
) )
...@@ -30,11 +31,11 @@ import ( ...@@ -30,11 +31,11 @@ import (
// Resolver resolves dependencies from semantic version ranges to a particular version. // Resolver resolves dependencies from semantic version ranges to a particular version.
type Resolver struct { type Resolver struct {
chartpath string chartpath string
helmhome string helmhome helmpath.Home
} }
// New creates a new resolver for a given chart and a given helm home. // New creates a new resolver for a given chart and a given helm home.
func New(chartpath string, helmhome string) *Resolver { func New(chartpath string, helmhome helmpath.Home) *Resolver {
return &Resolver{ return &Resolver{
chartpath: chartpath, chartpath: chartpath,
helmhome: helmhome, helmhome: helmhome,
...@@ -43,7 +44,7 @@ func New(chartpath string, helmhome string) *Resolver { ...@@ -43,7 +44,7 @@ func New(chartpath string, helmhome string) *Resolver {
// Resolve resolves dependencies and returns a lock file with the resolution. // Resolve resolves dependencies and returns a lock file with the resolution.
func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.RequirementsLock, error) { func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.RequirementsLock, error) {
d, err := hashReq(reqs) d, err := HashReq(reqs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -54,7 +55,7 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement ...@@ -54,7 +55,7 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement
// Right now, we're just copying one entry to another. What we need to // Right now, we're just copying one entry to another. What we need to
// do here is parse the requirement as a SemVer range, and then look up // do here is parse the requirement as a SemVer range, and then look up
// whether a version in index.yaml satisfies this constraint. If so, // whether a version in index.yaml satisfies this constraint. If so,
// we need to clone the dep, settinv Version appropriately. // we need to clone the dep, setting Version appropriately.
// If not, we need to error out. // If not, we need to error out.
if _, err := semver.NewVersion(d.Version); err != nil { if _, err := semver.NewVersion(d.Version); err != nil {
return nil, fmt.Errorf("dependency %q has an invalid version: %s", d.Name, err) return nil, fmt.Errorf("dependency %q has an invalid version: %s", d.Name, err)
...@@ -73,11 +74,11 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement ...@@ -73,11 +74,11 @@ func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.Requirement
}, nil }, nil
} }
// hashReq generates a hash of the requirements. // HashReq generates a hash of the requirements.
// //
// This should be used only to compare against another hash generated by this // This should be used only to compare against another hash generated by this
// function. // function.
func hashReq(req *chartutil.Requirements) (string, error) { func HashReq(req *chartutil.Requirements) (string, error) {
data, err := json.Marshal(req) data, err := json.Marshal(req)
if err != nil { if err != nil {
return "", err return "", err
......
...@@ -66,7 +66,7 @@ func TestResolve(t *testing.T) { ...@@ -66,7 +66,7 @@ func TestResolve(t *testing.T) {
t.Fatalf("Expected error in test %q", tt.name) t.Fatalf("Expected error in test %q", tt.name)
} }
if h, err := hashReq(tt.req); err != nil { if h, err := HashReq(tt.req); err != nil {
t.Fatal(err) t.Fatal(err)
} else if h != l.Digest { } else if h != l.Digest {
t.Errorf("%q: hashes don't match.", tt.name) t.Errorf("%q: hashes don't match.", tt.name)
...@@ -97,7 +97,7 @@ func TestHashReq(t *testing.T) { ...@@ -97,7 +97,7 @@ func TestHashReq(t *testing.T) {
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
}, },
} }
h, err := hashReq(req) h, err := HashReq(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -106,7 +106,7 @@ func TestHashReq(t *testing.T) { ...@@ -106,7 +106,7 @@ func TestHashReq(t *testing.T) {
} }
req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}} req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}}
h, err = hashReq(req) h, err = HashReq(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -20,6 +20,8 @@ import ( ...@@ -20,6 +20,8 @@ import (
"io" "io"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
) )
const verifyDesc = ` const verifyDesc = `
...@@ -63,5 +65,6 @@ func newVerifyCmd(out io.Writer) *cobra.Command { ...@@ -63,5 +65,6 @@ func newVerifyCmd(out io.Writer) *cobra.Command {
} }
func (v *verifyCmd) run() error { func (v *verifyCmd) run() error {
return verifyChart(v.chartfile, v.keyring) _, err := downloader.VerifyChart(v.chartfile, v.keyring)
return err
} }
...@@ -71,3 +71,13 @@ func Expand(dir string, r io.Reader) error { ...@@ -71,3 +71,13 @@ func Expand(dir string, r io.Reader) error {
} }
return nil return nil
} }
// ExpandFile expands the src file into the dest directroy.
func ExpandFile(dest, src string) error {
h, err := os.Open(src)
if err != nil {
return err
}
defer h.Close()
return Expand(dest, h)
}
...@@ -120,6 +120,10 @@ func loadFiles(files []*afile) (*chart.Chart, error) { ...@@ -120,6 +120,10 @@ func loadFiles(files []*afile) (*chart.Chart, error) {
} else if strings.HasPrefix(f.name, "templates/") { } else if strings.HasPrefix(f.name, "templates/") {
c.Templates = append(c.Templates, &chart.Template{Name: f.name, Data: f.data}) c.Templates = append(c.Templates, &chart.Template{Name: f.name, Data: f.data})
} else if strings.HasPrefix(f.name, "charts/") { } else if strings.HasPrefix(f.name, "charts/") {
if filepath.Ext(f.name) == ".prov" {
c.Files = append(c.Files, &any.Any{TypeUrl: f.name, Value: f.data})
continue
}
cname := strings.TrimPrefix(f.name, "charts/") cname := strings.TrimPrefix(f.name, "charts/")
parts := strings.SplitN(cname, "/", 2) parts := strings.SplitN(cname, "/", 2)
scname := parts[0] scname := parts[0]
......
...@@ -51,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) { ...@@ -51,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected 1 template, got %d", len(c.Templates)) t.Errorf("Expected 1 template, got %d", len(c.Templates))
} }
numfiles := 7 numfiles := 8
if len(c.Files) != numfiles { if len(c.Files) != numfiles {
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
for _, n := range c.Files { for _, n := range c.Files {
...@@ -115,6 +115,31 @@ func verifyRequirements(t *testing.T, c *chart.Chart) { ...@@ -115,6 +115,31 @@ func verifyRequirements(t *testing.T, c *chart.Chart) {
} }
} }
} }
func verifyRequirementsLock(t *testing.T, c *chart.Chart) {
r, err := LoadRequirementsLock(c)
if err != nil {
t.Fatal(err)
}
if len(r.Dependencies) != 2 {
t.Errorf("Expected 2 requirements, got %d", len(r.Dependencies))
}
tests := []*Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
}
for i, tt := range tests {
d := r.Dependencies[i]
if d.Name != tt.Name {
t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
}
if d.Version != tt.Version {
t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
}
if d.Repository != tt.Repository {
t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
}
}
}
func verifyFrobnitz(t *testing.T, c *chart.Chart) { func verifyFrobnitz(t *testing.T, c *chart.Chart) {
verifyChartfile(t, c.Metadata) verifyChartfile(t, c.Metadata)
......
...@@ -24,6 +24,18 @@ import ( ...@@ -24,6 +24,18 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
) )
const (
requirementsName = "requirements.yaml"
lockfileName = "requirements.lock"
)
var (
// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
ErrRequirementsNotFound = errors.New(requirementsName + " not found")
// ErrLockfileNotFound indicates that a requirements.lock is not found.
ErrLockfileNotFound = errors.New(lockfileName + " not found")
)
// Dependency describes a chart upon which another chart depends. // Dependency describes a chart upon which another chart depends.
// //
// Dependencies can be used to express developer intent, or to capture the state // Dependencies can be used to express developer intent, or to capture the state
...@@ -65,14 +77,11 @@ type RequirementsLock struct { ...@@ -65,14 +77,11 @@ type RequirementsLock struct {
Dependencies []*Dependency `json:"dependencies"` Dependencies []*Dependency `json:"dependencies"`
} }
// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
var ErrRequirementsNotFound = errors.New("requirements.yaml not found")
// LoadRequirements loads a requirements file from an in-memory chart. // LoadRequirements loads a requirements file from an in-memory chart.
func LoadRequirements(c *chart.Chart) (*Requirements, error) { func LoadRequirements(c *chart.Chart) (*Requirements, error) {
var data []byte var data []byte
for _, f := range c.Files { for _, f := range c.Files {
if f.TypeUrl == "requirements.yaml" { if f.TypeUrl == requirementsName {
data = f.Value data = f.Value
} }
} }
...@@ -82,3 +91,18 @@ func LoadRequirements(c *chart.Chart) (*Requirements, error) { ...@@ -82,3 +91,18 @@ func LoadRequirements(c *chart.Chart) (*Requirements, error) {
r := &Requirements{} r := &Requirements{}
return r, yaml.Unmarshal(data, r) return r, yaml.Unmarshal(data, r)
} }
// LoadRequirementsLock loads a requirements lock file.
func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
var data []byte
for _, f := range c.Files {
if f.TypeUrl == lockfileName {
data = f.Value
}
}
if len(data) == 0 {
return nil, ErrLockfileNotFound
}
r := &RequirementsLock{}
return r, yaml.Unmarshal(data, r)
}
...@@ -25,3 +25,11 @@ func TestLoadRequirements(t *testing.T) { ...@@ -25,3 +25,11 @@ func TestLoadRequirements(t *testing.T) {
} }
verifyRequirements(t, c) verifyRequirements(t, c)
} }
func TestLoadRequirementsLock(t *testing.T) {
c, err := Load("testdata/frobnitz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
verifyRequirementsLock(t, c)
}
dependencies:
- name: alpine
version: "0.1.0"
repository: https://example.com/charts
- name: mariner
version: "4.3.2"
repository: https://example.com/charts
digest: invalid
...@@ -49,7 +49,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { ...@@ -49,7 +49,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
URL: baseURL + "/" + filename, URL: baseURL + "/" + filename,
Chartfile: md, Chartfile: md,
Digest: digest, Digest: digest,
// FIXME: Need to add Created Created: nowString(),
} }
i.Entries[name] = cr i.Entries[name] = cr
} }
...@@ -93,7 +93,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { ...@@ -93,7 +93,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
return index, nil return index, nil
} }
// DownloadIndexFile uses // DownloadIndexFile fetches the index from a repository.
func DownloadIndexFile(repoName, url, indexFilePath string) error { func DownloadIndexFile(repoName, url, indexFilePath string) error {
var indexURL string var indexURL string
......
...@@ -147,7 +147,7 @@ func (r *ChartRepository) Index() error { ...@@ -147,7 +147,7 @@ func (r *ChartRepository) Index() error {
if ok && ref.Created != "" { if ok && ref.Created != "" {
created = ref.Created created = ref.Created
} else { } else {
created = time.Now().UTC().String() created = nowString()
} }
url, _ := url.Parse(r.URL) url, _ := url.Parse(r.URL)
...@@ -171,6 +171,11 @@ func (r *ChartRepository) Index() error { ...@@ -171,6 +171,11 @@ func (r *ChartRepository) Index() error {
return r.saveIndexFile() return r.saveIndexFile()
} }
func nowString() string {
// FIXME: This is a different date format than we use elsewhere.
return time.Now().UTC().String()
}
func generateDigest(path string) (string, error) { func generateDigest(path string) (string, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
......
/*
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 repotest provides utilities for testing.
The server provides a testing server that can be set up and torn down quickly.
*/
package repotest
/*
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 repotest
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/repo"
)
// NewServer creates a repository server for testing.
//
// docroot should be a temp dir managed by the caller.
//
// This will start the server, serving files off of the docroot.
//
// Use CopyCharts to move charts into the repository and then index them
// for service.
func NewServer(docroot string) *Server {
root, err := filepath.Abs(docroot)
if err != nil {
panic(err)
}
srv := &Server{
docroot: root,
}
srv.start()
// Add the testing repository as the only repo.
if err := setTestingRepository(docroot, "test", srv.URL()); err != nil {
panic(err)
}
return srv
}
// Server is an implementaiton of a repository server for testing.
type Server struct {
docroot string
srv *httptest.Server
}
// Root gets the docroot for the server.
func (s *Server) Root() string {
return s.docroot
}
// CopyCharts takes a glob expression and copies those charts to the server root.
func (s *Server) CopyCharts(origin string) ([]string, error) {
files, err := filepath.Glob(origin)
if err != nil {
return []string{}, err
}
copied := make([]string, len(files))
for i, f := range files {
base := filepath.Base(f)
newname := filepath.Join(s.docroot, base)
data, err := ioutil.ReadFile(f)
if err != nil {
return []string{}, err
}
if err := ioutil.WriteFile(newname, data, 0755); err != nil {
return []string{}, err
}
copied[i] = newname
}
// generate the index
index, err := repo.IndexDirectory(s.docroot, s.URL())
if err != nil {
return copied, err
}
d, err := yaml.Marshal(index.Entries)
if err != nil {
return copied, err
}
ifile := filepath.Join(s.docroot, "index.yaml")
err = ioutil.WriteFile(ifile, d, 0755)
return copied, err
}
func (s *Server) start() {
s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot)))
}
// Stop stops the server and closes all connections.
//
// It should be called explicitly.
func (s *Server) Stop() {
s.srv.Close()
}
// URL returns the URL of the server.
//
// Example:
// http://localhost:1776
func (s *Server) URL() string {
return s.srv.URL
}
// setTestingRepository sets up a testing repository.yaml with only the given name/URL.
func setTestingRepository(helmhome, name, url string) error {
// Oddly, there is no repo.Save function for this.
data, err := yaml.Marshal(&map[string]string{name: url})
if err != nil {
return err
}
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
dest := filepath.Join(helmhome, "repository/repositories.yaml")
return ioutil.WriteFile(dest, data, 0666)
}
/*
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 repotest
import (
"io/ioutil"
"net/http"
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v2"
"k8s.io/helm/pkg/repo"
)
// Young'n, in these here parts, we test our tests.
func TestServer(t *testing.T) {
docroot, err := ioutil.TempDir("", "helm-repotest-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(docroot)
srv := NewServer(docroot)
defer srv.Stop()
c, err := srv.CopyCharts("testdata/*.tgz")
if err != nil {
// Some versions of Go don't correctly fire defer on Fatal.
t.Error(err)
return
}
if len(c) != 1 {
t.Errorf("Unexpected chart count: %d", len(c))
}
if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" {
t.Errorf("Unexpected chart: %s", c[0])
}
res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz")
if err != nil {
t.Error(err)
return
}
if res.ContentLength < 500 {
t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength)
}
res, err = http.Get(srv.URL() + "/index.yaml")
if err != nil {
t.Error(err)
return
}
data, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Error(err)
return
}
var m map[string]*repo.ChartRef
if err := yaml.Unmarshal(data, &m); err != nil {
t.Error(err)
return
}
if l := len(m); l != 1 {
t.Errorf("Expected 1 entry, got %d", l)
return
}
expect := "examplechart-0.1.0"
if m[expect].Name != "examplechart-0.1.0" {
t.Errorf("Unexpected chart: %s", m[expect].Name)
}
if m[expect].Chartfile.Name != "examplechart" {
t.Errorf("Unexpected chart: %s", m[expect].Chartfile.Name)
}
res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing")
if err != nil {
t.Error(err)
return
}
if res.StatusCode != 404 {
t.Errorf("Expected 404, got %d", res.StatusCode)
}
}
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
description: A Helm chart for Kubernetes
name: examplechart
version: 0.1.0
# Default values for examplechart.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment