mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
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.
This commit is contained in:
parent
a5921faf99
commit
593718d749
52 changed files with 1701 additions and 520 deletions
|
|
@ -27,6 +27,11 @@ import (
|
|||
"k8s.io/helm/pkg/chartutil"
|
||||
)
|
||||
|
||||
const (
|
||||
reqLock = "requirements.lock"
|
||||
reqYaml = "requirements.yaml"
|
||||
)
|
||||
|
||||
const dependencyDesc = `
|
||||
Manage the dependencies of a chart.
|
||||
|
||||
|
|
@ -74,7 +79,7 @@ if it cannot find a requirements.yaml.
|
|||
|
||||
func newDependencyCmd(out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dependency update|list",
|
||||
Use: "dependency update|build|list",
|
||||
Aliases: []string{"dep", "dependencies"},
|
||||
Short: "manage a chart's dependencies",
|
||||
Long: dependencyDesc,
|
||||
|
|
@ -82,6 +87,7 @@ func newDependencyCmd(out io.Writer) *cobra.Command {
|
|||
|
||||
cmd.AddCommand(newDependencyListCmd(out))
|
||||
cmd.AddCommand(newDependencyUpdateCmd(out))
|
||||
cmd.AddCommand(newDependencyBuildCmd(out))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -146,10 +152,14 @@ func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string {
|
|||
if err != nil {
|
||||
return "corrupt"
|
||||
}
|
||||
if c.Metadata.Name == dep.Name && c.Metadata.Version == dep.Version {
|
||||
return "ok"
|
||||
if c.Metadata.Name != dep.Name {
|
||||
return "misnamed"
|
||||
}
|
||||
return "mismatch"
|
||||
|
||||
if c.Metadata.Version != dep.Version {
|
||||
return "wrong version"
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
folder := filepath.Join(l.chartpath, "charts", dep.Name)
|
||||
|
|
@ -196,6 +206,14 @@ func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Wr
|
|||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f)
|
||||
|
|
|
|||
85
cmd/helm/dependency_build.go
Normal file
85
cmd/helm/dependency_build.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
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()
|
||||
}
|
||||
115
cmd/helm/dependency_build_test.go
Normal file
115
cmd/helm/dependency_build_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
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.
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/resolver"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/repo"
|
||||
"k8s.io/helm/cmd/helm/downloader"
|
||||
"k8s.io/helm/cmd/helm/helmpath"
|
||||
)
|
||||
|
||||
const dependencyUpDesc = `
|
||||
|
|
@ -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',
|
||||
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'
|
||||
type dependencyUpdateCmd struct {
|
||||
out io.Writer
|
||||
chartpath string
|
||||
repoFile string
|
||||
repopath string
|
||||
helmhome string
|
||||
helmhome helmpath.Home
|
||||
verify bool
|
||||
keyring string
|
||||
}
|
||||
|
|
@ -74,16 +66,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
duc.helmhome = homePath()
|
||||
duc.repoFile = repositoriesFile()
|
||||
duc.repopath = repositoryDirectory()
|
||||
duc.helmhome = helmpath.Home(homePath())
|
||||
|
||||
return duc.run()
|
||||
},
|
||||
}
|
||||
|
||||
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.")
|
||||
|
||||
return cmd
|
||||
|
|
@ -91,197 +81,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
|
|||
|
||||
// run runs the full dependency update process.
|
||||
func (d *dependencyUpdateCmd) run() error {
|
||||
if fi, err := os.Stat(d.chartpath); err != nil {
|
||||
return fmt.Errorf("could not find %s: %s", d.chartpath, err)
|
||||
} else if !fi.IsDir() {
|
||||
return errors.New("only unpacked charts can be updated")
|
||||
man := &downloader.Manager{
|
||||
Out: d.out,
|
||||
ChartPath: d.chartpath,
|
||||
HelmHome: d.helmhome,
|
||||
Keyring: d.keyring,
|
||||
}
|
||||
c, err := chartutil.LoadDir(d.chartpath)
|
||||
if err != nil {
|
||||
return err
|
||||
if d.verify {
|
||||
man.Verify = downloader.VerifyIfPossible
|
||||
}
|
||||
|
||||
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 len(repos) > 0 {
|
||||
// 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 ioutil.WriteFile(dest, data, 0755)
|
||||
return man.Update()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -27,10 +25,12 @@ import (
|
|||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/helm/cmd/helm/helmpath"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
"k8s.io/helm/pkg/provenance"
|
||||
"k8s.io/helm/pkg/repo"
|
||||
"k8s.io/helm/pkg/repo/repotest"
|
||||
)
|
||||
|
||||
func TestDependencyUpdateCmd(t *testing.T) {
|
||||
|
|
@ -40,36 +40,35 @@ func TestDependencyUpdateCmd(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
helmHome = hh // Shoot me now.
|
||||
helmHome = hh
|
||||
defer func() {
|
||||
os.RemoveAll(hh)
|
||||
helmHome = oldhome
|
||||
}()
|
||||
|
||||
srv := newTestingRepositoryServer(hh)
|
||||
defer srv.stop()
|
||||
copied, err := srv.copyCharts("testdata/testcharts/*.tgz")
|
||||
t.Logf("Copied charts %s", strings.Join(copied, "\n"))
|
||||
t.Logf("Listening for directory %s", srv.docroot)
|
||||
srv := repotest.NewServer(hh)
|
||||
defer srv.Stop()
|
||||
copied, err := srv.CopyCharts("testdata/testcharts/*.tgz")
|
||||
t.Logf("Copied charts:\n%s", strings.Join(copied, "\n"))
|
||||
t.Logf("Listening on directory %s", srv.Root())
|
||||
|
||||
chartname := "depup"
|
||||
if err := createTestingChart(hh, chartname, srv.url()); err != nil {
|
||||
if err := createTestingChart(hh, chartname, srv.URL()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
duc := &dependencyUpdateCmd{out: out}
|
||||
duc.helmhome = hh
|
||||
duc.helmhome = helmpath.Home(hh)
|
||||
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 {
|
||||
output := out.String()
|
||||
t.Logf("Output: %s", output)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
t.Logf("Output: %s", output)
|
||||
// This is written directly to stdout, so we have to capture as is.
|
||||
if !strings.Contains(output, `update from the "test" chart repository`) {
|
||||
t.Errorf("Repo did not get updated\n%s", output)
|
||||
|
|
@ -98,95 +97,6 @@ func TestDependencyUpdateCmd(t *testing.T) {
|
|||
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
|
||||
//
|
||||
// The baseURL can be used to point to a particular repository server.
|
||||
|
|
|
|||
196
cmd/helm/downloader/chart_downloader.go
Normal file
196
cmd/helm/downloader/chart_downloader.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
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"
|
||||
}
|
||||
149
cmd/helm/downloader/chart_downloader_test.go
Normal file
149
cmd/helm/downloader/chart_downloader_test.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
}
|
||||
23
cmd/helm/downloader/doc.go
Normal file
23
cmd/helm/downloader/doc.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
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
|
||||
330
cmd/helm/downloader/manager.go
Normal file
330
cmd/helm/downloader/manager.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
BIN
cmd/helm/downloader/testdata/helm-test-key.pub
vendored
Normal file
BIN
cmd/helm/downloader/testdata/helm-test-key.pub
vendored
Normal file
Binary file not shown.
BIN
cmd/helm/downloader/testdata/helm-test-key.secret
vendored
Normal file
BIN
cmd/helm/downloader/testdata/helm-test-key.secret
vendored
Normal file
Binary file not shown.
38
cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml
vendored
Normal file
38
cmd/helm/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
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: ""
|
||||
1
cmd/helm/downloader/testdata/helmhome/repository/cache/local-index.yaml
vendored
Symbolic link
1
cmd/helm/downloader/testdata/helmhome/repository/cache/local-index.yaml
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
repository/local/index.yaml
|
||||
0
cmd/helm/downloader/testdata/helmhome/repository/local/index.yaml
vendored
Normal file
0
cmd/helm/downloader/testdata/helmhome/repository/local/index.yaml
vendored
Normal file
1
cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml
vendored
Normal file
1
cmd/helm/downloader/testdata/helmhome/repository/repositories.yaml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
testing: "http://example.com"
|
||||
BIN
cmd/helm/downloader/testdata/signtest-0.1.0.tgz
vendored
Normal file
BIN
cmd/helm/downloader/testdata/signtest-0.1.0.tgz
vendored
Normal file
Binary file not shown.
20
cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov
vendored
Executable file
20
cmd/helm/downloader/testdata/signtest-0.1.0.tgz.prov
vendored
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
-----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-----
|
||||
5
cmd/helm/downloader/testdata/signtest/.helmignore
vendored
Normal file
5
cmd/helm/downloader/testdata/signtest/.helmignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 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
|
||||
3
cmd/helm/downloader/testdata/signtest/Chart.yaml
vendored
Executable file
3
cmd/helm/downloader/testdata/signtest/Chart.yaml
vendored
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
description: A Helm chart for Kubernetes
|
||||
name: signtest
|
||||
version: 0.1.0
|
||||
6
cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml
vendored
Executable file
6
cmd/helm/downloader/testdata/signtest/alpine/Chart.yaml
vendored
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
description: Deploy a basic Alpine Linux pod
|
||||
home: https://k8s.io/helm
|
||||
name: alpine
|
||||
sources:
|
||||
- https://github.com/kubernetes/helm
|
||||
version: 0.1.0
|
||||
9
cmd/helm/downloader/testdata/signtest/alpine/README.md
vendored
Executable file
9
cmd/helm/downloader/testdata/signtest/alpine/README.md
vendored
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
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`.
|
||||
16
cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml
vendored
Executable file
16
cmd/helm/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml
vendored
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
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"]
|
||||
2
cmd/helm/downloader/testdata/signtest/alpine/values.yaml
vendored
Executable file
2
cmd/helm/downloader/testdata/signtest/alpine/values.yaml
vendored
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
# The pod name
|
||||
name: my-alpine
|
||||
10
cmd/helm/downloader/testdata/signtest/templates/pod.yaml
vendored
Normal file
10
cmd/helm/downloader/testdata/signtest/templates/pod.yaml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: signtest
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: waiter
|
||||
image: "alpine:3.3"
|
||||
command: ["/bin/sleep","9000"]
|
||||
0
cmd/helm/downloader/testdata/signtest/values.yaml
vendored
Normal file
0
cmd/helm/downloader/testdata/signtest/values.yaml
vendored
Normal file
|
|
@ -17,21 +17,16 @@ limitations under the License.
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/provenance"
|
||||
"k8s.io/helm/pkg/repo"
|
||||
)
|
||||
|
||||
const fetchDesc = `
|
||||
|
|
@ -53,6 +48,7 @@ type fetchCmd struct {
|
|||
untar bool
|
||||
untardir string
|
||||
chartRef string
|
||||
destdir string
|
||||
|
||||
verify bool
|
||||
keyring string
|
||||
|
|
@ -83,9 +79,10 @@ func newFetchCmd(out io.Writer) *cobra.Command {
|
|||
|
||||
f := cmd.Flags()
|
||||
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.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
|
||||
}
|
||||
|
|
@ -96,162 +93,60 @@ func (f *fetchCmd) run() error {
|
|||
pname += ".tgz"
|
||||
}
|
||||
|
||||
return downloadAndSaveChart(pname, f.untar, f.untardir, f.verify, f.keyring)
|
||||
}
|
||||
|
||||
// downloadAndSaveChart fetches a chart over HTTP, and then (if verify is true) verifies it.
|
||||
//
|
||||
// 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) {
|
||||
r, err := repo.LoadRepositoriesFile(repositoriesFile())
|
||||
if err != nil {
|
||||
return bytes.NewBuffer(nil), err
|
||||
c := downloader.ChartDownloader{
|
||||
HelmHome: helmpath.Home(homePath()),
|
||||
Out: f.out,
|
||||
Keyring: f.keyring,
|
||||
Verify: downloader.VerifyNever,
|
||||
}
|
||||
|
||||
// get download url
|
||||
u, err := mapRepoArg(pname, r.Repositories)
|
||||
if err != nil {
|
||||
return bytes.NewBuffer(nil), err
|
||||
if f.verify {
|
||||
c.Verify = downloader.VerifyAlways
|
||||
}
|
||||
|
||||
href := u.String()
|
||||
buf, err := fetchChart(href)
|
||||
if err != nil {
|
||||
return buf, err
|
||||
}
|
||||
|
||||
if verify {
|
||||
basename := filepath.Base(pname)
|
||||
sigref := href + ".prov"
|
||||
sig, err := fetchChart(sigref)
|
||||
// If untar is set, we fetch to a tempdir, then untar and copy after
|
||||
// verification.
|
||||
dest := f.destdir
|
||||
if f.untar {
|
||||
var err error
|
||||
dest, err = ioutil.TempDir("", "helm-")
|
||||
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 fmt.Errorf("Failed to untar: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(dest)
|
||||
}
|
||||
|
||||
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
|
||||
} 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 _, err := os.Stat(provfile); err != nil {
|
||||
return fmt.Errorf("could not load provenance file %s: %s", provfile, err)
|
||||
}
|
||||
|
||||
sig, err := provenance.NewFromKeyring(keyring, "")
|
||||
v, err := c.DownloadTo(pname, dest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load keyring: %s", err)
|
||||
return err
|
||||
}
|
||||
ver, err := sig.Verify(path, provfile)
|
||||
if flagDebug {
|
||||
for name := range ver.SignedBy.Identities {
|
||||
fmt.Printf("Signed by %q\n", name)
|
||||
|
||||
if f.verify {
|
||||
fmt.Fprintf(f.out, "Verification: %v", v)
|
||||
}
|
||||
|
||||
// After verification, untar the chart into the requested directory.
|
||||
if f.untar {
|
||||
ud := f.untardir
|
||||
if !filepath.IsAbs(ud) {
|
||||
ud = filepath.Join(f.destdir, ud)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("Failed to untar: %s is not a directory", ud)
|
||||
}
|
||||
|
||||
from := filepath.Join(dest, filepath.Base(pname))
|
||||
return chartutil.ExpandFile(ud, from)
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, "/")
|
||||
return saveChartFile(p[len(p)-1], buf)
|
||||
}
|
||||
|
||||
// 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)
|
||||
resp.Body.Close()
|
||||
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
|
||||
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 {
|
||||
// Grab the chart name that we'll use for the name of the file to download to.
|
||||
out, err := os.Create(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,49 +17,109 @@ limitations under the License.
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"k8s.io/helm/pkg/repo/repotest"
|
||||
)
|
||||
|
||||
type testCase struct {
|
||||
in string
|
||||
expectedErr error
|
||||
expectedOut string
|
||||
}
|
||||
func TestFetchCmd(t *testing.T) {
|
||||
hh, err := tempHelmHome()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := homePath()
|
||||
helmHome = hh
|
||||
defer func() {
|
||||
helmHome = old
|
||||
os.RemoveAll(hh)
|
||||
}()
|
||||
|
||||
var repos = map[string]string{
|
||||
"local": "http://localhost:8879/charts",
|
||||
"someother": "http://storage.googleapis.com/mycharts",
|
||||
}
|
||||
|
||||
var testCases = []testCase{
|
||||
{"bad", fmt.Errorf("Invalid chart url format: bad"), ""},
|
||||
{"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) {
|
||||
u, err := mapRepoArg(tc.in, repos)
|
||||
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)
|
||||
// all flags will get "--home=TMDIR -d outdir" appended.
|
||||
tests := []struct {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
if (u == nil && len(tc.expectedOut) != 0) ||
|
||||
(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)
|
||||
srv := repotest.NewServer(hh)
|
||||
defer srv.Stop()
|
||||
|
||||
if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
t.Logf("HELM_HOME=%s", homePath())
|
||||
|
||||
func TestMappings(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
testRunner(t, tc)
|
||||
for _, tt := range tests {
|
||||
outdir := filepath.Join(hh, "testout")
|
||||
os.RemoveAll(outdir)
|
||||
os.Mkdir(outdir, 0755)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
cmd := newFetchCmd(buf)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
cmd/helm/helmpath/helmhome.go
Normal file
61
cmd/helm/helmpath/helmhome.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
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")
|
||||
}
|
||||
36
cmd/helm/helmpath/helmhome_test.go
Normal file
36
cmd/helm/helmpath/helmhome_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
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 (
|
|||
"github.com/ghodss/yaml"
|
||||
"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/proto/hapi/release"
|
||||
"k8s.io/helm/pkg/timeconv"
|
||||
|
|
@ -286,7 +288,7 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
|
|||
if fi.IsDir() {
|
||||
return "", errors.New("cannot verify a directory")
|
||||
}
|
||||
if err := verifyChart(abs, keyring); err != nil {
|
||||
if _, err := downloader.VerifyChart(abs, keyring); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -306,7 +308,17 @@ func locateChartPath(name string, verify bool, keyring string) (string, error) {
|
|||
if filepath.Ext(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))
|
||||
if err != nil {
|
||||
return lname, err
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ var testName = "test-name"
|
|||
func TestRepoAddCmd(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, "OK")
|
||||
fmt.Fprintln(w, "")
|
||||
}))
|
||||
|
||||
tests := []releaseCase{
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/Masterminds/semver"
|
||||
|
||||
"k8s.io/helm/cmd/helm/helmpath"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/provenance"
|
||||
)
|
||||
|
|
@ -30,11 +31,11 @@ import (
|
|||
// Resolver resolves dependencies from semantic version ranges to a particular version.
|
||||
type Resolver struct {
|
||||
chartpath string
|
||||
helmhome string
|
||||
helmhome helmpath.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{
|
||||
chartpath: chartpath,
|
||||
helmhome: helmhome,
|
||||
|
|
@ -43,7 +44,7 @@ func New(chartpath string, helmhome string) *Resolver {
|
|||
|
||||
// Resolve resolves dependencies and returns a lock file with the resolution.
|
||||
func (r *Resolver) Resolve(reqs *chartutil.Requirements) (*chartutil.RequirementsLock, error) {
|
||||
d, err := hashReq(reqs)
|
||||
d, err := HashReq(reqs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -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
|
||||
// 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,
|
||||
// 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 _, err := semver.NewVersion(d.Version); err != nil {
|
||||
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
|
|||
}, 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
|
||||
// function.
|
||||
func hashReq(req *chartutil.Requirements) (string, error) {
|
||||
func HashReq(req *chartutil.Requirements) (string, error) {
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func TestResolve(t *testing.T) {
|
|||
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)
|
||||
} else if h != l.Digest {
|
||||
t.Errorf("%q: hashes don't match.", tt.name)
|
||||
|
|
@ -97,7 +97,7 @@ func TestHashReq(t *testing.T) {
|
|||
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
|
||||
},
|
||||
}
|
||||
h, err := hashReq(req)
|
||||
h, err := HashReq(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ func TestHashReq(t *testing.T) {
|
|||
}
|
||||
|
||||
req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}}
|
||||
h, err = hashReq(req)
|
||||
h, err = HashReq(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import (
|
|||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/downloader"
|
||||
)
|
||||
|
||||
const verifyDesc = `
|
||||
|
|
@ -63,5 +65,6 @@ func newVerifyCmd(out io.Writer) *cobra.Command {
|
|||
}
|
||||
|
||||
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 {
|
|||
}
|
||||
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) {
|
|||
} else if strings.HasPrefix(f.name, "templates/") {
|
||||
c.Templates = append(c.Templates, &chart.Template{Name: f.name, Data: f.data})
|
||||
} 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/")
|
||||
parts := strings.SplitN(cname, "/", 2)
|
||||
scname := parts[0]
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) {
|
|||
t.Errorf("Expected 1 template, got %d", len(c.Templates))
|
||||
}
|
||||
|
||||
numfiles := 7
|
||||
numfiles := 8
|
||||
if len(c.Files) != numfiles {
|
||||
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
|
||||
for _, n := range c.Files {
|
||||
|
|
@ -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) {
|
||||
verifyChartfile(t, c.Metadata)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@ import (
|
|||
"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.
|
||||
//
|
||||
// Dependencies can be used to express developer intent, or to capture the state
|
||||
|
|
@ -65,14 +77,11 @@ type RequirementsLock struct {
|
|||
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.
|
||||
func LoadRequirements(c *chart.Chart) (*Requirements, error) {
|
||||
var data []byte
|
||||
for _, f := range c.Files {
|
||||
if f.TypeUrl == "requirements.yaml" {
|
||||
if f.TypeUrl == requirementsName {
|
||||
data = f.Value
|
||||
}
|
||||
}
|
||||
|
|
@ -82,3 +91,18 @@ func LoadRequirements(c *chart.Chart) (*Requirements, error) {
|
|||
r := &Requirements{}
|
||||
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) {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
BIN
pkg/chartutil/testdata/frobnitz-1.2.3.tgz
vendored
BIN
pkg/chartutil/testdata/frobnitz-1.2.3.tgz
vendored
Binary file not shown.
Binary file not shown.
8
pkg/chartutil/testdata/frobnitz/requirements.lock
vendored
Normal file
8
pkg/chartutil/testdata/frobnitz/requirements.lock
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
Binary file not shown.
|
|
@ -49,7 +49,7 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
|
|||
URL: baseURL + "/" + filename,
|
||||
Chartfile: md,
|
||||
Digest: digest,
|
||||
// FIXME: Need to add Created
|
||||
Created: nowString(),
|
||||
}
|
||||
i.Entries[name] = cr
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
|
|||
return index, nil
|
||||
}
|
||||
|
||||
// DownloadIndexFile uses
|
||||
// DownloadIndexFile fetches the index from a repository.
|
||||
func DownloadIndexFile(repoName, url, indexFilePath string) error {
|
||||
var indexURL string
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func (r *ChartRepository) Index() error {
|
|||
if ok && ref.Created != "" {
|
||||
created = ref.Created
|
||||
} else {
|
||||
created = time.Now().UTC().String()
|
||||
created = nowString()
|
||||
}
|
||||
|
||||
url, _ := url.Parse(r.URL)
|
||||
|
|
@ -171,6 +171,11 @@ func (r *ChartRepository) Index() error {
|
|||
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) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
|
|
|||
20
pkg/repo/repotest/doc.go
Normal file
20
pkg/repo/repotest/doc.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
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
|
||||
130
pkg/repo/repotest/server.go
Normal file
130
pkg/repo/repotest/server.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
107
pkg/repo/repotest/server_test.go
Normal file
107
pkg/repo/repotest/server_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
BIN
pkg/repo/repotest/testdata/examplechart-0.1.0.tgz
vendored
Normal file
BIN
pkg/repo/repotest/testdata/examplechart-0.1.0.tgz
vendored
Normal file
Binary file not shown.
21
pkg/repo/repotest/testdata/examplechart/.helmignore
vendored
Normal file
21
pkg/repo/repotest/testdata/examplechart/.helmignore
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# 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
|
||||
3
pkg/repo/repotest/testdata/examplechart/Chart.yaml
vendored
Executable file
3
pkg/repo/repotest/testdata/examplechart/Chart.yaml
vendored
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
description: A Helm chart for Kubernetes
|
||||
name: examplechart
|
||||
version: 0.1.0
|
||||
4
pkg/repo/repotest/testdata/examplechart/values.yaml
vendored
Normal file
4
pkg/repo/repotest/testdata/examplechart/values.yaml
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Default values for examplechart.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare name/value pairs to be passed into your templates.
|
||||
# name: value
|
||||
Loading…
Reference in a new issue