mirror of https://github.com/helm/helm
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.pull/1221/head
parent
a5921faf99
commit
593718d749
@ -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,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.
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: ""
|
@ -0,0 +1 @@
|
||||
repository/local/index.yaml
|
@ -0,0 +1 @@
|
||||
testing: "http://example.com"
|
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"]
|
@ -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")
|
||||
}
|
Binary file not shown.
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
|
Binary file not shown.
@ -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)
|
||||
}
|
||||
}
|
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: 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…
Reference in new issue