Merge pull request #1221 from technosophos/feat/helm-dependency-update-2

feat(helm): add requirements.yaml support
pull/1235/head
Matt Butcher 8 years ago committed by GitHub
commit be409d31c5

@ -0,0 +1,234 @@
/*
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 (
"fmt"
"io"
"os"
"path/filepath"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/chartutil"
)
const (
reqLock = "requirements.lock"
reqYaml = "requirements.yaml"
)
const dependencyDesc = `
Manage the dependencies of a chart.
Helm charts store their dependencies in 'charts/'. For chart developers, it is
often easier to manage a single dependency file ('requirements.yaml')
which declares all dependencies.
The dependency commands operate on that file, making it easy to synchronize
between the desired dependencies and the actual dependencies stored in the
'charts/' directory.
A 'requirements.yaml' file is a YAML file in which developers can declare chart
dependencies, along with the location of the chart and the desired version.
For example, this requirements file declares two dependencies:
# requirements.yaml
dependencies:
- name: nginx
version: "1.2.3"
repository: "https://example.com/charts"
- name: memcached
version: "3.2.1"
repository: "https://another.example.com/charts"
The 'name' should be the name of a chart, where that name must match the name
in that chart's 'Chart.yaml' file.
The 'version' field should contain a semantic version or version range.
The 'repository' URL should point to a Chart Repository. Helm expects that by
appending '/index.yaml' to the URL, it should be able to retrieve the chart
repository's index. Note: 'repository' cannot be a repository alias. It must be
a URL.
`
const dependencyListDesc = `
List all of the dependencies declared in a chart.
This can take chart archives and chart directories as input. It will not alter
the contents of a chart.
This will produce an error if the chart cannot be loaded. It will emit a warning
if it cannot find a requirements.yaml.
`
func newDependencyCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "dependency update|build|list",
Aliases: []string{"dep", "dependencies"},
Short: "manage a chart's dependencies",
Long: dependencyDesc,
}
cmd.AddCommand(newDependencyListCmd(out))
cmd.AddCommand(newDependencyUpdateCmd(out))
cmd.AddCommand(newDependencyBuildCmd(out))
return cmd
}
type dependencyListCmd struct {
out io.Writer
chartpath string
}
func newDependencyListCmd(out io.Writer) *cobra.Command {
dlc := &dependencyListCmd{
out: out,
}
cmd := &cobra.Command{
Use: "list [flags] CHART",
Aliases: []string{"ls"},
Short: "list the dependencies for the given chart",
Long: dependencyListDesc,
RunE: func(cmd *cobra.Command, args []string) error {
cp := "."
if len(args) > 0 {
cp = args[0]
}
var err error
dlc.chartpath, err = filepath.Abs(cp)
if err != nil {
return err
}
return dlc.run()
},
}
return cmd
}
func (l *dependencyListCmd) run() error {
c, err := chartutil.Load(l.chartpath)
if err != nil {
return err
}
r, err := chartutil.LoadRequirements(c)
if err != nil {
if err == chartutil.ErrRequirementsNotFound {
fmt.Fprintf(l.out, "WARNING: no requirements at %s/charts", l.chartpath)
return nil
}
return err
}
l.printRequirements(r, l.out)
fmt.Fprintln(l.out)
l.printMissing(r, l.out)
return nil
}
func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string {
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, dep.Version)
archive := filepath.Join(l.chartpath, "charts", filename)
if _, err := os.Stat(archive); err == nil {
c, err := chartutil.Load(archive)
if err != nil {
return "corrupt"
}
if c.Metadata.Name != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
return "wrong version"
}
return "ok"
}
folder := filepath.Join(l.chartpath, "charts", dep.Name)
if fi, err := os.Stat(folder); err != nil {
return "missing"
} else if !fi.IsDir() {
return "mispackaged"
}
c, err := chartutil.Load(folder)
if err != nil {
return "corrupt"
}
if c.Metadata.Name != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
return "wrong version"
}
return "unpacked"
}
// printRequirements prints all of the requirements in the yaml file.
func (l *dependencyListCmd) printRequirements(reqs *chartutil.Requirements, out io.Writer) {
table := uitable.New()
table.MaxColWidth = 80
table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS")
for _, row := range reqs.Dependencies {
table.AddRow(row.Name, row.Version, row.Repository, l.dependencyStatus(row))
}
fmt.Fprintln(out, table)
}
// printMissing prints warnings about charts that are present on disk, but are not in the requirements.
func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements, out io.Writer) {
folder := filepath.Join(l.chartpath, "charts/*")
files, err := filepath.Glob(folder)
if err != nil {
fmt.Fprintln(l.out, err)
return
}
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)
continue
}
found := false
for _, d := range reqs.Dependencies {
if d.Name == c.Metadata.Name {
found = true
break
}
}
if !found {
fmt.Fprintf(l.out, "WARNING: %q is not in requirements.yaml.\n", f)
}
}
}

@ -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()
}

@ -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)
}
}

@ -0,0 +1,71 @@
/*
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"
"strings"
"testing"
)
func TestDependencyListCmd(t *testing.T) {
tests := []struct {
name string
args []string
expect string
err bool
}{
{
name: "No such chart",
args: []string{"/no/such/chart"},
err: true,
},
{
name: "No requirements.yaml",
args: []string{"testdata/testcharts/alpine"},
expect: "WARNING: no requirements at ",
},
{
name: "Requirements in chart dir",
args: []string{"testdata/testcharts/reqtest"},
expect: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tunpacked\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tunpacked\n",
},
{
name: "Requirements in chart archive",
args: []string{"testdata/testcharts/reqtest-0.1.0.tgz"},
expect: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tmissing\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tmissing\n",
},
}
for _, tt := range tests {
buf := bytes.NewBuffer(nil)
dlc := newDependencyListCmd(buf)
if err := dlc.RunE(dlc, tt.args); err != nil {
if tt.err {
continue
}
t.Errorf("Test %q: %s", tt.name, err)
continue
}
got := buf.String()
if !strings.Contains(got, tt.expect) {
t.Errorf("Test: %q, Expected:\n%q\nGot:\n%q", tt.name, tt.expect, got)
}
}
}

@ -0,0 +1,94 @@
/*
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"
"path/filepath"
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/downloader"
"k8s.io/helm/cmd/helm/helmpath"
)
const dependencyUpDesc = `
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
helmhome helmpath.Home
verify bool
keyring string
}
// newDependencyUpdateCmd creates a new dependency update command.
func newDependencyUpdateCmd(out io.Writer) *cobra.Command {
duc := &dependencyUpdateCmd{
out: out,
}
cmd := &cobra.Command{
Use: "update [flags] CHART",
Aliases: []string{"up"},
Short: "update charts/ based on the contents of requirements.yaml",
Long: dependencyUpDesc,
RunE: func(cmd *cobra.Command, args []string) error {
cp := "."
if len(args) > 0 {
cp = args[0]
}
var err error
duc.chartpath, err = filepath.Abs(cp)
if err != nil {
return err
}
duc.helmhome = helmpath.Home(homePath())
return duc.run()
},
}
f := cmd.Flags()
f.BoolVar(&duc.verify, "verify", false, "Verify the packages against signatures.")
f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "The keyring containing public keys.")
return cmd
}
// run runs the full dependency update process.
func (d *dependencyUpdateCmd) 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.Update()
}

@ -0,0 +1,124 @@
/*
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"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"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) {
// Set up a testing helm home
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()
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 {
t.Fatal(err)
}
out := bytes.NewBuffer(nil)
duc := &dependencyUpdateCmd{out: out}
duc.helmhome = helmpath.Home(hh)
duc.chartpath = filepath.Join(hh, chartname)
if err := duc.run(); err != nil {
output := out.String()
t.Logf("Output: %s", output)
t.Fatal(err)
}
output := out.String()
// 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)
}
// 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)
}
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)
}
t.Logf("Results: %s", out.String())
}
// createTestingChart creates a basic chart that depends on reqtest-0.1.0
//
// The baseURL can be used to point to a particular repository server.
func createTestingChart(dest, name, baseURL string) error {
cfile := &chart.Metadata{
Name: name,
Version: "1.2.3",
}
dir := filepath.Join(dest, name)
_, err := chartutil.Create(cfile, dest)
if err != nil {
return err
}
req := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "reqtest", Version: "0.1.0", Repository: baseURL},
},
}
data, err := yaml.Marshal(req)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(dir, "requirements.yaml"), data, 0655)
}

@ -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"
}

@ -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
}
}

@ -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

@ -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)
}

Binary file not shown.

@ -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: ""

Binary file not shown.

@ -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-----

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: signtest
version: 0.1.0

@ -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

@ -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`.

@ -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"]

@ -0,0 +1,2 @@
# The pod name
name: my-alpine

@ -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"]

@ -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,154 +93,60 @@ func (f *fetchCmd) run() error {
pname += ".tgz"
}
return downloadChart(pname, f.untar, f.untardir, f.verify, f.keyring)
}
// downloadChart 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 downloadChart(pname string, untar bool, untardir string, verify bool, keyring string) error {
r, err := repo.LoadRepositoriesFile(repositoriesFile())
if err != nil {
return err
}
// get download url
u, err := mapRepoArg(pname, r.Repositories)
if err != nil {
return err
c := downloader.ChartDownloader{
HelmHome: helmpath.Home(homePath()),
Out: f.out,
Keyring: f.keyring,
Verify: downloader.VerifyNever,
}
href := u.String()
buf, err := fetchChart(href)
if err != nil {
return err
if f.verify {
c.Verify = downloader.VerifyAlways
}
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 fmt.Errorf("provenance data not downloaded from %s: %s", sigref, err)
}
if err := ioutil.WriteFile(basename+".prov", sig.Bytes(), 0755); err != nil {
return fmt.Errorf("provenance data not saved: %s", err)
}
if err := verifyChart(basename, keyring); err != nil {
return err
return fmt.Errorf("Failed to untar: %s", err)
}
defer os.RemoveAll(dest)
}
return saveChart(pname, buf, untar, untardir)
}
// 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 {
v, err := c.DownloadTo(pname, dest)
if 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)
if f.verify {
fmt.Fprintf(f.out, "Verification: %v", v)
}
sig, err := provenance.NewFromKeyring(keyring, "")
if err != nil {
return fmt.Errorf("failed to load keyring: %s", err)
}
ver, err := sig.Verify(path, provfile)
if flagDebug {
for name := range ver.SignedBy.Identities {
fmt.Printf("Signed by %q\n", name)
// 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",
}
// 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,
},
}
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"), ""},
}
srv := repotest.NewServer(hh)
defer srv.Stop()
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)
if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil {
t.Fatal(err)
}
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)
}
t.Logf("HELM_HOME=%s", homePath())
}
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
}
func TestMappings(t *testing.T) {
for _, tc := range testCases {
testRunner(t, tc)
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)
}
}
}

@ -100,6 +100,7 @@ func newRootCmd(out io.Writer) *cobra.Command {
newUpdateCmd(out),
newVersionCmd(nil, out),
newRepoCmd(out),
newDependencyCmd(out),
)
return cmd
}

@ -20,6 +20,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"math/rand"
"regexp"
"testing"
@ -205,3 +206,22 @@ type releaseCase struct {
err bool
resp *release.Release
}
// tmpHelmHome sets up a Helm Home in a temp dir.
//
// This does not clean up the directory. You must do that yourself.
// You must also set helmHome yourself.
func tempHelmHome() (string, error) {
oldhome := helmHome
dir, err := ioutil.TempDir("", "helm_home-")
if err != nil {
return "n/", err
}
helmHome = dir
if err := ensureHome(); err != nil {
return "n/", err
}
helmHome = oldhome
return dir, nil
}

@ -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")
}

@ -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")
}

@ -28,7 +28,7 @@ import (
func TestEnsureHome(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, "")
}))
defaultRepositoryURL = ts.URL

@ -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 := downloadChart(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{
@ -57,7 +57,7 @@ func TestRepoAddCmd(t *testing.T) {
func TestRepoAdd(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, "")
}))
helmHome, _ = ioutil.TempDir("", "helm_home")

@ -0,0 +1,88 @@
/*
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 resolver
import (
"bytes"
"encoding/json"
"fmt"
"time"
"github.com/Masterminds/semver"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/provenance"
)
// Resolver resolves dependencies from semantic version ranges to a particular version.
type Resolver struct {
chartpath string
helmhome helmpath.Home
}
// New creates a new resolver for a given chart and a given helm home.
func New(chartpath string, helmhome helmpath.Home) *Resolver {
return &Resolver{
chartpath: chartpath,
helmhome: helmhome,
}
}
// 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)
if err != nil {
return nil, err
}
// Now we clone the dependencies, locking as we go.
locked := make([]*chartutil.Dependency, len(reqs.Dependencies))
for i, d := range reqs.Dependencies {
// 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, 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)
}
locked[i] = &chartutil.Dependency{
Name: d.Name,
Repository: d.Repository,
Version: d.Version,
}
}
return &chartutil.RequirementsLock{
Generated: time.Now(),
Digest: d,
Dependencies: locked,
}, nil
}
// 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) {
data, err := json.Marshal(req)
if err != nil {
return "", err
}
s, err := provenance.Digest(bytes.NewBuffer(data))
return "sha256:" + s, err
}

@ -0,0 +1,116 @@
/*
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 resolver
import (
"testing"
"k8s.io/helm/pkg/chartutil"
)
func TestResolve(t *testing.T) {
tests := []struct {
name string
req *chartutil.Requirements
expect *chartutil.RequirementsLock
err bool
}{
{
name: "version failure",
req: &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "oedipus-rex", Repository: "http://example.com", Version: ">1"},
},
},
err: true,
},
{
name: "valid lock",
req: &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "antigone", Repository: "http://example.com", Version: "1.0.0"},
},
},
expect: &chartutil.RequirementsLock{
Dependencies: []*chartutil.Dependency{
{Name: "antigone", Repository: "http://example.com", Version: "1.0.0"},
},
},
},
}
r := New("testdata/chartpath", "testdata/helmhome")
for _, tt := range tests {
l, err := r.Resolve(tt.req)
if err != nil {
if tt.err {
continue
}
t.Fatal(err)
}
if tt.err {
t.Fatalf("Expected error in test %q", tt.name)
}
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)
}
// Check fields.
if len(l.Dependencies) != len(tt.req.Dependencies) {
t.Errorf("%s: wrong number of dependencies in lock", tt.name)
}
d0 := l.Dependencies[0]
e0 := tt.expect.Dependencies[0]
if d0.Name != e0.Name {
t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name)
}
if d0.Repository != e0.Repository {
t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository)
}
if d0.Version != e0.Version {
t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version)
}
}
}
func TestHashReq(t *testing.T) {
expect := "sha256:e70e41f8922e19558a8bf62f591a8b70c8e4622e3c03e5415f09aba881f13885"
req := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},
},
}
h, err := HashReq(req)
if err != nil {
t.Fatal(err)
}
if expect != h {
t.Errorf("Expected %q, got %q", expect, h)
}
req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}}
h, err = HashReq(req)
if err != nil {
t.Fatal(err)
}
if expect == h {
t.Errorf("Expected %q != %q", expect, h)
}
}

@ -45,6 +45,7 @@ func search(cmd *cobra.Command, args []string) error {
return errors.New("This command needs at least one argument (search string)")
}
// TODO: This needs to be refactored to use loadChartRepositories
results, err := searchCacheForPattern(cacheDirectory(), args[0])
if err != nil {
return err

Binary file not shown.

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: reqtest
version: 0.1.0

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: reqsubchart
version: 0.1.0

@ -0,0 +1,4 @@
# Default values for reqsubchart.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: reqsubchart2
version: 0.2.0

@ -0,0 +1,4 @@
# Default values for reqsubchart.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

@ -0,0 +1,3 @@
dependencies: []
digest: Not implemented
generated: 2016-09-13T17:25:17.593788787-06:00

@ -0,0 +1,7 @@
dependencies:
- name: reqsubchart
version: 0.1.0
repository: "https://example.com/charts"
- name: reqsubchart2
version: 0.2.0
repository: "https://example.com/charts"

@ -0,0 +1,4 @@
# Default values for reqtest.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
# name: value

@ -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
}

@ -125,6 +125,7 @@ chart's `charts/` directory:
```
wordpress:
Chart.yaml
requirements.yaml
# ...
charts/
apache/
@ -142,6 +143,61 @@ directory.
**TIP:** _To drop a dependency into your `charts/` directory, use the
`helm fetch` command._
### Managing Dependencies with `requirements.yaml`
While Helm will allow you to manually manage your dependencies, the
preferred method of declaring dependencies is by using a
`requirements.yaml` file inside of your chart.
A `requirements.yaml` file is a simple file for listing your
dependencies.
```yaml
dependencies:
- name: apache
version: 1.2.3
repository: http://example.com/charts
- name: mysql
version: 3.2.1
repository: http://another.example.com/charts
```
- The `name` field is the name of the chart you want.
- The `version` field is the version of the chart you want.
- The `repository` field is the full URL to the chart repository. Note
that you must also use `helm repo add` to add that repo locally.
Once you have a dependencies file, you can run `helm dependency update`
and it will use your dependency file to download all of the specified
charts into your `charts/` directory for you.
```console
$ helm dep up foochart
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "local" chart repository
...Successfully got an update from the "stable" chart repository
...Successfully got an update from the "example" chart repository
...Successfully got an update from the "another" chart repository
Update Complete. Happy Helming!
Saving 2 charts
Downloading apache from repo http://example.com/charts
Downloading mysql from repo http://another.example.com/charts
```
When `helm dependency update` retrieves charts, it will store them as
chart archives in the `charts/` directory. So for the example above, one
would expect to see the following files in the charts directory:
```
charts/
apache-1.2.3.tgz
mysql-3.2.1.tgz
```
Manging charts with `requirements.yaml` is a good way to easily keep
charts updated, and also share requirements information throughout a
team.
## Templates and Values
By default, Helm Chart templates are written in the Go template language, with the

@ -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]

@ -29,6 +29,7 @@ func TestLoadDir(t *testing.T) {
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyRequirements(t, c)
}
func TestLoadFile(t *testing.T) {
@ -38,6 +39,7 @@ func TestLoadFile(t *testing.T) {
}
verifyFrobnitz(t, c)
verifyChart(t, c)
verifyRequirements(t, c)
}
func verifyChart(t *testing.T, c *chart.Chart) {
@ -49,7 +51,7 @@ func verifyChart(t *testing.T, c *chart.Chart) {
t.Errorf("Expected 1 template, got %d", len(c.Templates))
}
numfiles := 6
numfiles := 8
if len(c.Files) != numfiles {
t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
for _, n := range c.Files {
@ -88,6 +90,57 @@ func verifyChart(t *testing.T, c *chart.Chart) {
}
func verifyRequirements(t *testing.T, c *chart.Chart) {
r, err := LoadRequirements(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 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)

@ -0,0 +1,108 @@
/*
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 chartutil
import (
"errors"
"time"
"github.com/ghodss/yaml"
"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
// of a chart.
type Dependency struct {
// Name is the name of the dependency.
//
// This must mach the name in the dependency's Chart.yaml.
Name string `json:"name"`
// Version is the version (range) of this chart.
//
// A lock file will always produce a single version, while a dependency
// may contain a semantic version range.
Version string `json:"version,omitempty"`
// The URL to the repository.
//
// Appending `index.yaml` to this string should result in a URL that can be
// used to fetch the repository index.
Repository string `json:"repository"`
}
// Requirements is a list of requirements for a chart.
//
// Requirements are charts upon which this chart depends. This expresses
// developer intent.
type Requirements struct {
Dependencies []*Dependency `json:"dependencies"`
}
// RequirementsLock is a lock file for requirements.
//
// It represents the state that the dependencies should be in.
type RequirementsLock struct {
// Genderated is the date the lock file was last generated.
Generated time.Time `json:"generated"`
// Digest is a hash of the requirements file used to generate it.
Digest string `json:"digest"`
// Dependencies is the list of dependencies that this lock file has locked.
Dependencies []*Dependency `json:"dependencies"`
}
// 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 == requirementsName {
data = f.Value
}
}
if len(data) == 0 {
return nil, ErrRequirementsNotFound
}
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)
}

@ -0,0 +1,35 @@
/*
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 chartutil
import (
"testing"
)
func TestLoadRequirements(t *testing.T) {
c, err := Load("testdata/frobnitz")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
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)
}

Binary file not shown.

@ -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

@ -0,0 +1,7 @@
dependencies:
- name: alpine
version: "0.1.0"
repository: https://example.com/charts
- name: mariner
version: "4.3.2"
repository: https://example.com/charts

@ -0,0 +1,4 @@
dependencies:
- name: albatross
repository: https://example.com/mariner/charts
version: "0.1.0"

@ -204,7 +204,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
ver.SignedBy = by
// Second, verify the hash of the tarball.
sum, err := sumArchive(chartpath)
sum, err := DigestFile(chartpath)
if err != nil {
return ver, err
}
@ -254,7 +254,7 @@ func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, er
func messageBlock(chartpath string) (*bytes.Buffer, error) {
var b *bytes.Buffer
// Checksum the archive
chash, err := sumArchive(chartpath)
chash, err := DigestFile(chartpath)
if err != nil {
return b, err
}
@ -332,20 +332,26 @@ func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
return openpgp.ReadKeyRing(f)
}
// sumArchive calculates a SHA256 hash (like Docker) for a given file.
// DigestFile calculates a SHA256 hash (like Docker) for a given file.
//
// It takes the path to the archive file, and returns a string representation of
// the SHA256 sum.
//
// The intended use of this function is to generate a sum of a chart TGZ file.
func sumArchive(filename string) (string, error) {
func DigestFile(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
return Digest(f)
}
// Digest hashes a reader and returns a SHA256 digest.
//
// Helm uses SHA256 as its default hash for all non-cryptographic applications.
func Digest(in io.Reader) (string, error) {
hash := crypto.SHA256.New()
io.Copy(hash, f)
io.Copy(hash, in)
return hex.EncodeToString(hash.Sum(nil)), nil
}

@ -127,6 +127,28 @@ func TestLoadKeyRing(t *testing.T) {
}
}
func TestDigest(t *testing.T) {
f, err := os.Open(testChartfile)
if err != nil {
t.Fatal(err)
}
defer f.Close()
hash, err := Digest(f)
if err != nil {
t.Fatal(err)
}
sig, err := readSumFile(testSumfile)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(sig, hash) {
t.Errorf("Expected %s to be in %s", hash, sig)
}
}
func TestNewFromFiles(t *testing.T) {
s, err := NewFromFiles(testKeyfile, testPubfile)
if err != nil {
@ -138,8 +160,8 @@ func TestNewFromFiles(t *testing.T) {
}
}
func TestSumArchive(t *testing.T) {
hash, err := sumArchive(testChartfile)
func TestDigestFile(t *testing.T) {
hash, err := DigestFile(testChartfile)
if err != nil {
t.Fatal(err)
}

@ -19,11 +19,14 @@ package repo
import (
"io/ioutil"
"net/http"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/provenance"
)
var indexPath = "index.yaml"
@ -33,17 +36,64 @@ type IndexFile struct {
Entries map[string]*ChartRef
}
// NewIndexFile initializes an index.
func NewIndexFile() *IndexFile {
return &IndexFile{Entries: map[string]*ChartRef{}}
}
// Add adds a file to the index
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
name := strings.TrimSuffix(filename, ".tgz")
cr := &ChartRef{
Name: name,
URL: baseURL + "/" + filename,
Chartfile: md,
Digest: digest,
Created: nowString(),
}
i.Entries[name] = cr
}
// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2
// ChartRef represents a chart entry in the IndexFile
type ChartRef struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
Created string `yaml:"created,omitempty"`
Removed bool `yaml:"removed,omitempty"`
Digest string `yaml:"digest,omitempty"`
Chartfile *chart.Metadata `yaml:"chartfile"`
Name string `yaml:"name" json:"name"`
URL string `yaml:"url" json:"url"`
Created string `yaml:"created,omitempty" json:"created,omitempty"`
Removed bool `yaml:"removed,omitempty" json:"removed,omitempty"`
Digest string `yaml:"digest,omitempty" json:"digest,omitempty"`
Chartfile *chart.Metadata `yaml:"chartfile" json:"chartfile"`
}
// IndexDirectory reads a (flat) directory and generates an index.
//
// It indexes only charts that have been packaged (*.tgz).
//
// It writes the results to dir/index.yaml.
func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
if err != nil {
return nil, err
}
index := NewIndexFile()
for _, arch := range archives {
fname := filepath.Base(arch)
c, err := chartutil.Load(arch)
if err != nil {
// Assume this is not a chart.
continue
}
hash, err := provenance.DigestFile(arch)
if err != nil {
return index, err
}
index.Add(c.Metadata, fname, baseURL, hash)
}
return index, nil
}
// DownloadIndexFile uses
// DownloadIndexFile fetches the index from a repository.
func DownloadIndexFile(repoName, url, indexFilePath string) error {
var indexURL string
@ -72,9 +122,7 @@ func DownloadIndexFile(repoName, url, indexFilePath string) error {
func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error {
var refs map[string]*ChartRef
if err := unmarshal(&refs); err != nil {
if _, ok := err.(*yaml.TypeError); !ok {
return err
}
return err
}
i.Entries = refs
return nil
@ -101,11 +149,11 @@ func LoadIndexFile(path string) (*IndexFile, error) {
return nil, err
}
var indexfile IndexFile
err = yaml.Unmarshal(b, &indexfile)
indexfile := NewIndexFile()
err = yaml.Unmarshal(b, indexfile)
if err != nil {
return nil, err
}
return &indexfile, nil
return indexfile, nil
}

@ -110,3 +110,35 @@ func TestLoadIndexFile(t *testing.T) {
t.Errorf("alpine entry was not decoded properly")
}
}
func TestIndexDirectory(t *testing.T) {
dir := "testdata/repository"
index, err := IndexDirectory(dir, "http://localhost:8080")
if err != nil {
t.Fatal(err)
}
if l := len(index.Entries); l != 2 {
t.Fatalf("Expected 2 entries, got %d", l)
}
// Other things test the entry generation more thoroughly. We just test a
// few fields.
cname := "frobnitz-1.2.3"
frob, ok := index.Entries[cname]
if !ok {
t.Fatalf("Could not read chart %s", cname)
}
if len(frob.Digest) == 0 {
t.Errorf("Missing digest of file %s.", frob.Name)
}
if frob.Chartfile == nil {
t.Fatalf("Chartfile %s not added to index.", cname)
}
if frob.URL != "http://localhost:8080/frobnitz-1.2.3.tgz" {
t.Errorf("Unexpected URL: %s", frob.URL)
}
if frob.Chartfile.Name != "frobnitz" {
t.Errorf("Expected frobnitz, got %q", frob.Chartfile.Name)
}
}

@ -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 {

@ -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

@ -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)
}

@ -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)
}
}

@ -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

@ -0,0 +1,3 @@
description: A Helm chart for Kubernetes
name: examplechart
version: 0.1.0

@ -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…
Cancel
Save