diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index f28e8eff0..a05611d8c 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -16,18 +16,12 @@ limitations under the License. package main import ( - "fmt" "io" - "os" "path/filepath" - "github.com/Masterminds/semver" - "github.com/gosuri/uitable" "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/chart" - "k8s.io/helm/pkg/chart/loader" ) const dependencyDesc = ` @@ -103,12 +97,8 @@ func newDependencyCmd(out io.Writer) *cobra.Command { return cmd } -type dependencyLisOptions struct { - chartpath string -} - func newDependencyListCmd(out io.Writer) *cobra.Command { - o := &dependencyLisOptions{ + o := &refListOptions{ chartpath: ".", } @@ -122,148 +112,8 @@ func newDependencyListCmd(out io.Writer) *cobra.Command { if len(args) > 0 { o.chartpath = filepath.Clean(args[0]) } - return o.run(out) + return o.run(out, false) }, } return cmd } - -func (o *dependencyLisOptions) run(out io.Writer) error { - c, err := loader.Load(o.chartpath) - if err != nil { - return err - } - - if c.Metadata.Dependencies == nil { - fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(o.chartpath, "charts")) - return nil - } - - o.printDependencies(out, c.Metadata.Dependencies) - fmt.Fprintln(out) - o.printMissing(out, c.Metadata.Dependencies) - return nil -} - -func (o *dependencyLisOptions) dependencyStatus(dep *chart.Dependency) string { - filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") - archives, err := filepath.Glob(filepath.Join(o.chartpath, "charts", filename)) - if err != nil { - return "bad pattern" - } else if len(archives) > 1 { - return "too many matches" - } else if len(archives) == 1 { - archive := archives[0] - if _, err := os.Stat(archive); err == nil { - c, err := loader.Load(archive) - if err != nil { - return "corrupt" - } - if c.Name() != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "ok" - } - return "wrong version" - } - return "ok" - } - } - - folder := filepath.Join(o.chartpath, "charts", dep.Name) - if fi, err := os.Stat(folder); err != nil { - return "missing" - } else if !fi.IsDir() { - return "mispackaged" - } - - c, err := loader.Load(folder) - if err != nil { - return "corrupt" - } - - if c.Name() != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "unpacked" - } - return "wrong version" - } - - return "unpacked" -} - -// printDependencies prints all of the dependencies in the yaml file. -func (o *dependencyLisOptions) printDependencies(out io.Writer, reqs []*chart.Dependency) { - table := uitable.New() - table.MaxColWidth = 80 - table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") - for _, row := range reqs { - table.AddRow(row.Name, row.Version, row.Repository, o.dependencyStatus(row)) - } - fmt.Fprintln(out, table) -} - -// printMissing prints warnings about charts that are present on disk, but are -// not in Charts.yaml. -func (o *dependencyLisOptions) printMissing(out io.Writer, reqs []*chart.Dependency) { - folder := filepath.Join(o.chartpath, "charts/*") - files, err := filepath.Glob(folder) - if err != nil { - fmt.Fprintln(out, err) - return - } - - for _, f := range files { - fi, err := os.Stat(f) - if err != nil { - fmt.Fprintf(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 := loader.Load(f) - if err != nil { - fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) - continue - } - found := false - for _, d := range reqs { - if d.Name == c.Name() { - found = true - break - } - } - if !found { - fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) - } - } - -} diff --git a/cmd/helm/library.go b/cmd/helm/library.go index 4cb510af4..7f2e2efd0 100644 --- a/cmd/helm/library.go +++ b/cmd/helm/library.go @@ -17,6 +17,7 @@ package main import ( "io" + "path/filepath" "github.com/spf13/cobra" @@ -67,17 +68,47 @@ If the library chart is retrieved locally, it is not required to have the repository added to helm by "helm add repo". Version matching is also supported for this case. ` +const libraryListDesc = ` +List all of the libraries 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. +` func newLibraryCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "library update", + Use: "library update|list", Aliases: []string{"lib", "libraries"}, Short: "manage a chart's libraries", Long: libraryDesc, Args: require.NoArgs, } + cmd.AddCommand(newLibraryListCmd(out)) cmd.AddCommand(newLibraryUpdateCmd(out)) return cmd } + +func newLibraryListCmd(out io.Writer) *cobra.Command { + o := &refListOptions{ + chartpath: ".", + } + + cmd := &cobra.Command{ + Use: "list CHART", + Aliases: []string{"ls"}, + Short: "list the libraries for the given chart", + Long: libraryListDesc, + Args: require.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + o.chartpath = filepath.Clean(args[0]) + } + return o.run(out, true) + }, + } + return cmd +} diff --git a/cmd/helm/library_test.go b/cmd/helm/library_test.go new file mode 100644 index 000000000..8fef74882 --- /dev/null +++ b/cmd/helm/library_test.go @@ -0,0 +1,53 @@ +/* +Copyright The Helm Authors. +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 ( + "runtime" + "testing" +) + +func TestLibraryListCmd(t *testing.T) { + noSuchChart := cmdTestCase{ + name: "No such chart", + cmd: "library list /no/such/chart", + golden: "output/dependency-list-no-chart-linux.txt", + wantError: true, + } + + noDependencies := cmdTestCase{ + name: "No libraries", + cmd: "library list testdata/testcharts/alpine", + golden: "output/library-list-no-requirements-linux.txt", + } + + if runtime.GOOS == "windows" { + noSuchChart.golden = "output/dependency-list-no-chart-windows.txt" + noDependencies.golden = "output/library-list-no-requirements-windows.txt" + } + + tests := []cmdTestCase{noSuchChart, + noDependencies, { + name: "Libraries in library dir", + cmd: "library list testdata/testcharts/libcharttest", + golden: "output/dependency-list.txt", + }, { + name: "Libraries in chart archive", + cmd: "library list testdata/testcharts/libcharttest-0.1.0.tgz", + golden: "output/dependency-list-archive.txt", + }} + runTestCmd(t, tests) +} diff --git a/cmd/helm/reference.go b/cmd/helm/reference.go new file mode 100644 index 000000000..e5a3b3e5c --- /dev/null +++ b/cmd/helm/reference.go @@ -0,0 +1,187 @@ +/* +Copyright The Helm Authors. +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/Masterminds/semver" + "github.com/gosuri/uitable" + + "k8s.io/helm/pkg/chart" + "k8s.io/helm/pkg/chart/loader" +) + +type refListOptions struct { + chartpath string +} + +func (o *refListOptions) run(out io.Writer, isLib bool) error { + c, err := loader.Load(o.chartpath) + if err != nil { + return err + } + + var reqs []*chart.Dependency + var dirName string + if isLib { + reqs = c.Metadata.Libraries + dirName = "library" + } else { + reqs = c.Metadata.Dependencies + dirName = "charts" + } + + if reqs == nil { + if isLib { + fmt.Fprintf(out, "WARNING: no libraries at %s\n", filepath.Join(o.chartpath, dirName)) + } else { + fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(o.chartpath, dirName)) + } + return nil + } + + o.printDependencies(out, reqs, dirName) + fmt.Fprintln(out) + o.printMissing(out, reqs, dirName) + return nil +} + +func (o *refListOptions) dependencyStatus(dep *chart.Dependency, dirName string) string { + filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") + archives, err := filepath.Glob(filepath.Join(o.chartpath, dirName, filename)) + if err != nil { + return "bad pattern" + } else if len(archives) > 1 { + return "too many matches" + } else if len(archives) == 1 { + archive := archives[0] + if _, err := os.Stat(archive); err == nil { + c, err := loader.Load(archive) + if err != nil { + return "corrupt" + } + if c.Name() != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "ok" + } + return "wrong version" + } + return "ok" + } + } + + folder := filepath.Join(o.chartpath, dirName, dep.Name) + if fi, err := os.Stat(folder); err != nil { + return "missing" + } else if !fi.IsDir() { + return "mispackaged" + } + + c, err := loader.Load(folder) + if err != nil { + return "corrupt" + } + + if c.Name() != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if constraint.Check(v) { + return "unpacked" + } + return "wrong version" + } + + return "unpacked" +} + +// printDependencies prints all of the dependencies/libraries in the yaml file. +func (o *refListOptions) printDependencies(out io.Writer, reqs []*chart.Dependency, dirName string) { + table := uitable.New() + table.MaxColWidth = 80 + table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + for _, row := range reqs { + table.AddRow(row.Name, row.Version, row.Repository, o.dependencyStatus(row, dirName)) + } + fmt.Fprintln(out, table) +} + +// printMissing prints warnings about charts that are present on disk, but are +// not in Charts.yaml. +func (o *refListOptions) printMissing(out io.Writer, reqs []*chart.Dependency, dirName string) { + folder := filepath.Join(o.chartpath, dirName+"/*") + files, err := filepath.Glob(folder) + if err != nil { + fmt.Fprintln(out, err) + return + } + + for _, f := range files { + fi, err := os.Stat(f) + if err != nil { + fmt.Fprintf(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 := loader.Load(f) + if err != nil { + fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) + continue + } + found := false + for _, d := range reqs { + if d.Name == c.Name() { + found = true + break + } + } + if !found { + fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) + } + } + +} diff --git a/cmd/helm/testdata/output/library-list-no-requirements-linux.txt b/cmd/helm/testdata/output/library-list-no-requirements-linux.txt new file mode 100644 index 000000000..8e1ca31a6 --- /dev/null +++ b/cmd/helm/testdata/output/library-list-no-requirements-linux.txt @@ -0,0 +1 @@ +WARNING: no libraries at testdata/testcharts/alpine/library diff --git a/cmd/helm/testdata/output/library-list-no-requirements-windows.txt b/cmd/helm/testdata/output/library-list-no-requirements-windows.txt new file mode 100644 index 000000000..cd7495608 --- /dev/null +++ b/cmd/helm/testdata/output/library-list-no-requirements-windows.txt @@ -0,0 +1 @@ +WARNING: no libraries at testdata\testcharts\alpine\library diff --git a/cmd/helm/testdata/testcharts/libcharttest-0.1.0.tgz b/cmd/helm/testdata/testcharts/libcharttest-0.1.0.tgz new file mode 100644 index 000000000..29d7ae5e0 Binary files /dev/null and b/cmd/helm/testdata/testcharts/libcharttest-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/libcharttest/.helmignore b/cmd/helm/testdata/testcharts/libcharttest/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/.helmignore @@ -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 diff --git a/cmd/helm/testdata/testcharts/libcharttest/Chart.yaml b/cmd/helm/testdata/testcharts/libcharttest/Chart.yaml new file mode 100644 index 000000000..e25d307be --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/Chart.yaml @@ -0,0 +1,13 @@ +description: A Helm chart for Kubernetes +name: libcharttest +version: 0.1.0 +libraries: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" + - name: reqsubchart2 + version: 0.2.0 + repository: "https://example.com/charts" + - name: reqsubchart3 + version: ">=0.1.0" + repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/.helmignore b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/.helmignore @@ -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 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/Chart.yaml new file mode 100644 index 000000000..c3813bc8c --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/values.yaml b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart/values.yaml @@ -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 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/.helmignore b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/.helmignore new file mode 100644 index 000000000..f0c131944 --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/.helmignore @@ -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 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/Chart.yaml b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/Chart.yaml new file mode 100644 index 000000000..9f7c22a71 --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: reqsubchart2 +version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/values.yaml b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/values.yaml new file mode 100644 index 000000000..0f0b63f2a --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart2/values.yaml @@ -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 diff --git a/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart3-0.2.0.tgz b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart3-0.2.0.tgz new file mode 100644 index 000000000..84b0fb65e Binary files /dev/null and b/cmd/helm/testdata/testcharts/libcharttest/library/reqsubchart3-0.2.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/libcharttest/values.yaml b/cmd/helm/testdata/testcharts/libcharttest/values.yaml new file mode 100644 index 000000000..134357531 --- /dev/null +++ b/cmd/helm/testdata/testcharts/libcharttest/values.yaml @@ -0,0 +1,4 @@ +# Default values for libcharttest. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value