mirror of https://github.com/helm/helm
parent
dbb84a1b9e
commit
a5921faf99
@ -0,0 +1,216 @@
|
||||
/*
|
||||
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 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|list",
|
||||
Aliases: []string{"dep", "dependencies"},
|
||||
Short: "manage a chart's dependencies",
|
||||
Long: dependencyDesc,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newDependencyListCmd(out))
|
||||
cmd.AddCommand(newDependencyUpdateCmd(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 && c.Metadata.Version == dep.Version {
|
||||
return "ok"
|
||||
}
|
||||
return "mismatch"
|
||||
}
|
||||
|
||||
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 {
|
||||
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,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,287 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/helm/cmd/helm/resolver"
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/repo"
|
||||
)
|
||||
|
||||
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.
|
||||
`
|
||||
|
||||
// dependencyUpdateCmd describes a 'helm dependency update'
|
||||
type dependencyUpdateCmd struct {
|
||||
out io.Writer
|
||||
chartpath string
|
||||
repoFile string
|
||||
repopath string
|
||||
helmhome string
|
||||
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 = homePath()
|
||||
duc.repoFile = repositoriesFile()
|
||||
duc.repopath = repositoryDirectory()
|
||||
|
||||
return duc.run()
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.BoolVar(&duc.verify, "verify", false, "Verify the package against its signature.")
|
||||
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 {
|
||||
if fi, err := os.Stat(d.chartpath); err != nil {
|
||||
return fmt.Errorf("could not find %s: %s", d.chartpath, err)
|
||||
} else if !fi.IsDir() {
|
||||
return errors.New("only unpacked charts can be updated")
|
||||
}
|
||||
c, err := chartutil.LoadDir(d.chartpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := chartutil.LoadRequirements(c)
|
||||
if err != nil {
|
||||
if err == chartutil.ErrRequirementsNotFound {
|
||||
fmt.Fprintf(d.out, "No requirements found in %s/charts.\n", d.chartpath)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// For each repo in the file, update the cached copy of that repo
|
||||
if _, err := d.updateRepos(req.Dependencies); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now we need to find out which version of a chart best satisfies the
|
||||
// requirements the requirements.yaml
|
||||
lock, err := d.resolve(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now we need to fetch every package here into charts/
|
||||
if err := d.downloadAll(lock.Dependencies); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Finally, we need to write the lockfile.
|
||||
return writeLock(d.chartpath, lock)
|
||||
}
|
||||
|
||||
// resolve takes a list of requirements and translates them into an exact version to download.
|
||||
//
|
||||
// This returns a lock file, which has all of the requirements normalized to a specific version.
|
||||
func (d *dependencyUpdateCmd) resolve(req *chartutil.Requirements) (*chartutil.RequirementsLock, error) {
|
||||
res := resolver.New(d.chartpath, d.helmhome)
|
||||
return res.Resolve(req)
|
||||
}
|
||||
|
||||
// downloadAll takes a list of dependencies and downloads them into charts/
|
||||
func (d *dependencyUpdateCmd) downloadAll(deps []*chartutil.Dependency) error {
|
||||
repos, err := loadChartRepositories(d.repopath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(d.out, "Saving %d charts\n", len(deps))
|
||||
for _, dep := range deps {
|
||||
fmt.Fprintf(d.out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
|
||||
|
||||
target := fmt.Sprintf("%s-%s", dep.Name, dep.Version)
|
||||
churl, err := findChartURL(target, dep.Repository, repos)
|
||||
if err != nil {
|
||||
fmt.Fprintf(d.out, "WARNING: %s (skipped)", err)
|
||||
continue
|
||||
}
|
||||
|
||||
dest := filepath.Join(d.chartpath, "charts", target+".tgz")
|
||||
data, err := downloadChart(churl, d.verify, d.keyring)
|
||||
if err != nil {
|
||||
fmt.Fprintf(d.out, "WARNING: Could not download %s: %s (skipped)", churl, err)
|
||||
continue
|
||||
}
|
||||
if err := ioutil.WriteFile(dest, data.Bytes(), 0655); err != nil {
|
||||
fmt.Fprintf(d.out, "WARNING: %s (skipped)", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateRepos updates all of the local repos to their latest.
|
||||
//
|
||||
// If one of the dependencies present is not in the cached repos, this will error out. The
|
||||
// consequence of that is that every repository referenced in a requirements.yaml file
|
||||
// must also be added with 'helm repo add'.
|
||||
func (d *dependencyUpdateCmd) updateRepos(deps []*chartutil.Dependency) (*repo.RepoFile, error) {
|
||||
// TODO: In the future, we could make it so that only the repositories that
|
||||
// are used by this chart are updated. As it is, we're mainly doing some sanity
|
||||
// checking here.
|
||||
rf, err := repo.LoadRepositoriesFile(d.repoFile)
|
||||
if err != nil {
|
||||
return rf, err
|
||||
}
|
||||
repos := rf.Repositories
|
||||
|
||||
// Verify that all repositories referenced in the deps are actually known
|
||||
// by Helm.
|
||||
missing := []string{}
|
||||
for _, dd := range deps {
|
||||
found := false
|
||||
if dd.Repository == "" {
|
||||
found = true
|
||||
} else {
|
||||
for _, repo := range repos {
|
||||
if urlsAreEqual(repo, dd.Repository) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
missing = append(missing, dd.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
return rf, fmt.Errorf("no repository definition for %s. Try 'helm repo add'", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
if len(repos) > 0 {
|
||||
// This prints errors straight to out.
|
||||
updateCharts(repos, flagDebug, d.out)
|
||||
}
|
||||
return rf, nil
|
||||
}
|
||||
|
||||
// urlsAreEqual normalizes two URLs and then compares for equality.
|
||||
func urlsAreEqual(a, b string) bool {
|
||||
au, err := url.Parse(a)
|
||||
if err != nil {
|
||||
return a == b
|
||||
}
|
||||
bu, err := url.Parse(b)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return au.String() == bu.String()
|
||||
}
|
||||
|
||||
// findChartURL searches the cache of repo data for a chart that has the name and the repourl specified.
|
||||
//
|
||||
// In this current version, name is of the form 'foo-1.2.3'. This will change when
|
||||
// the repository index stucture changes.
|
||||
func findChartURL(name, repourl string, repos map[string]*repo.ChartRepository) (string, error) {
|
||||
for _, cr := range repos {
|
||||
if urlsAreEqual(repourl, cr.URL) {
|
||||
for ename, entry := range cr.IndexFile.Entries {
|
||||
if ename == name {
|
||||
return entry.URL, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("chart %s not found in %s", name, repourl)
|
||||
}
|
||||
|
||||
// loadChartRepositories reads the repositories.yaml, and then builds a map of
|
||||
// ChartRepositories.
|
||||
//
|
||||
// The key is the local name (which is only present in the repositories.yaml).
|
||||
func loadChartRepositories(repodir string) (map[string]*repo.ChartRepository, error) {
|
||||
indices := map[string]*repo.ChartRepository{}
|
||||
repoyaml := repositoriesFile()
|
||||
|
||||
// Load repositories.yaml file
|
||||
rf, err := repo.LoadRepositoriesFile(repoyaml)
|
||||
if err != nil {
|
||||
return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err)
|
||||
}
|
||||
|
||||
// localName: chartRepo
|
||||
for lname, url := range rf.Repositories {
|
||||
index, err := repo.LoadIndexFile(cacheIndexFile(lname))
|
||||
if err != nil {
|
||||
return indices, err
|
||||
}
|
||||
|
||||
cr := &repo.ChartRepository{
|
||||
URL: url,
|
||||
IndexFile: index,
|
||||
}
|
||||
indices[lname] = cr
|
||||
}
|
||||
return indices, nil
|
||||
}
|
||||
|
||||
// writeLock writes a lockfile to disk
|
||||
func writeLock(chartpath string, lock *chartutil.RequirementsLock) error {
|
||||
data, err := yaml.Marshal(lock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dest := filepath.Join(chartpath, "requirements.lock")
|
||||
return ioutil.WriteFile(dest, data, 0755)
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
/*
|
||||
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"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
"k8s.io/helm/pkg/provenance"
|
||||
"k8s.io/helm/pkg/repo"
|
||||
)
|
||||
|
||||
func TestDependencyUpdateCmd(t *testing.T) {
|
||||
// Set up a testing helm home
|
||||
oldhome := helmHome
|
||||
hh, err := tempHelmHome()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
helmHome = hh // Shoot me now.
|
||||
defer func() {
|
||||
os.RemoveAll(hh)
|
||||
helmHome = oldhome
|
||||
}()
|
||||
|
||||
srv := newTestingRepositoryServer(hh)
|
||||
defer srv.stop()
|
||||
copied, err := srv.copyCharts("testdata/testcharts/*.tgz")
|
||||
t.Logf("Copied charts %s", strings.Join(copied, "\n"))
|
||||
t.Logf("Listening for directory %s", srv.docroot)
|
||||
|
||||
chartname := "depup"
|
||||
if err := createTestingChart(hh, chartname, srv.url()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
duc := &dependencyUpdateCmd{out: out}
|
||||
duc.helmhome = hh
|
||||
duc.chartpath = filepath.Join(hh, chartname)
|
||||
duc.repoFile = filepath.Join(duc.helmhome, "repository/repositories.yaml")
|
||||
duc.repopath = filepath.Join(duc.helmhome, "repository")
|
||||
|
||||
if err := duc.run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
output := out.String()
|
||||
t.Logf("Output: %s", output)
|
||||
// This is written directly to stdout, so we have to capture as is.
|
||||
if !strings.Contains(output, `update from the "test" chart repository`) {
|
||||
t.Errorf("Repo did not get updated\n%s", output)
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// newTestingRepositoryServer creates a repository server for testing.
|
||||
//
|
||||
// docroot should be a temp dir managed by the caller.
|
||||
//
|
||||
// This will start the server, serving files off of the docroot.
|
||||
//
|
||||
// Use copyCharts to move charts into the repository and then index them
|
||||
// for service.
|
||||
func newTestingRepositoryServer(docroot string) *testingRepositoryServer {
|
||||
root, err := filepath.Abs(docroot)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
srv := &testingRepositoryServer{
|
||||
docroot: root,
|
||||
}
|
||||
srv.start()
|
||||
// Add the testing repository as the only repo.
|
||||
if err := setTestingRepository(docroot, "test", srv.url()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
type testingRepositoryServer struct {
|
||||
docroot string
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
// copyCharts takes a glob expression and copies those charts to the server root.
|
||||
func (s *testingRepositoryServer) copyCharts(origin string) ([]string, error) {
|
||||
files, err := filepath.Glob(origin)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
copied := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
base := filepath.Base(f)
|
||||
newname := filepath.Join(s.docroot, base)
|
||||
data, err := ioutil.ReadFile(f)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
if err := ioutil.WriteFile(newname, data, 0755); err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
copied[i] = newname
|
||||
}
|
||||
|
||||
// generate the index
|
||||
index, err := repo.IndexDirectory(s.docroot, s.url())
|
||||
if err != nil {
|
||||
return copied, err
|
||||
}
|
||||
|
||||
d, err := yaml.Marshal(index.Entries)
|
||||
if err != nil {
|
||||
return copied, err
|
||||
}
|
||||
|
||||
ifile := filepath.Join(s.docroot, "index.yaml")
|
||||
err = ioutil.WriteFile(ifile, d, 0755)
|
||||
return copied, err
|
||||
}
|
||||
|
||||
func (s *testingRepositoryServer) start() {
|
||||
s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot)))
|
||||
}
|
||||
|
||||
func (s *testingRepositoryServer) stop() {
|
||||
s.srv.Close()
|
||||
}
|
||||
|
||||
func (s *testingRepositoryServer) url() string {
|
||||
return s.srv.URL
|
||||
}
|
||||
|
||||
// setTestingRepository sets up a testing repository.yaml with only the given name/URL.
|
||||
func setTestingRepository(helmhome, name, url string) error {
|
||||
// Oddly, there is no repo.Save function for this.
|
||||
data, err := yaml.Marshal(&map[string]string{name: url})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.MkdirAll(filepath.Join(helmhome, "repository", name), 0755)
|
||||
dest := filepath.Join(helmhome, "repository/repositories.yaml")
|
||||
return ioutil.WriteFile(dest, data, 0666)
|
||||
}
|
||||
|
||||
// createTestingChart creates a basic chart that depends on reqtest-0.1.0
|
||||
//
|
||||
// The baseURL can be used to point to a particular repository server.
|
||||
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,87 @@
|
||||
/*
|
||||
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/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/provenance"
|
||||
)
|
||||
|
||||
// Resolver resolves dependencies from semantic version ranges to a particular version.
|
||||
type Resolver struct {
|
||||
chartpath string
|
||||
helmhome string
|
||||
}
|
||||
|
||||
// New creates a new resolver for a given chart and a given helm home.
|
||||
func New(chartpath string, helmhome string) *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, settinv 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)
|
||||
}
|
||||
}
|
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
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
|
||||
var ErrRequirementsNotFound = errors.New("requirements.yaml not found")
|
||||
|
||||
// LoadRequirements loads a requirements file from an in-memory chart.
|
||||
func LoadRequirements(c *chart.Chart) (*Requirements, error) {
|
||||
var data []byte
|
||||
for _, f := range c.Files {
|
||||
if f.TypeUrl == "requirements.yaml" {
|
||||
data = f.Value
|
||||
}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, ErrRequirementsNotFound
|
||||
}
|
||||
r := &Requirements{}
|
||||
return r, yaml.Unmarshal(data, r)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
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)
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -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
|
Binary file not shown.
@ -0,0 +1,4 @@
|
||||
dependencies:
|
||||
- name: albatross
|
||||
repository: https://example.com/mariner/charts
|
||||
version: "0.1.0"
|
Loading…
Reference in new issue