diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index 1ce07fb52..fb926f55a 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -22,9 +22,6 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/cmd/helm/require" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" ) const dependencyUpDesc = ` @@ -42,21 +39,9 @@ reason, an update command will not remove charts unless they are (a) present in the Chart.yaml file, but (b) at the wrong version. ` -// dependencyUpdateOptions describes a 'helm dependency update' -type dependencyUpdateOptions struct { - keyring string // --keyring - skipRefresh bool // --skip-refresh - verify bool // --verify - - // args - chartpath string - - helmhome helmpath.Home -} - // newDependencyUpdateCmd creates a new dependency update command. func newDependencyUpdateCmd(out io.Writer) *cobra.Command { - o := &dependencyUpdateOptions{ + o := &refUpdateOptions{ chartpath: ".", } @@ -71,7 +56,7 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { o.chartpath = filepath.Clean(args[0]) } o.helmhome = settings.Home - return o.run(out) + return o.run(out, false) }, } @@ -82,22 +67,3 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { return cmd } - -// run runs the full dependency update process. -func (o *dependencyUpdateOptions) run(out io.Writer) error { - man := &downloader.Manager{ - Out: out, - ChartPath: o.chartpath, - HelmHome: o.helmhome, - Keyring: o.keyring, - SkipUpdate: o.skipRefresh, - Getters: getter.All(settings), - } - if o.verify { - man.Verify = downloader.VerifyAlways - } - if settings.Debug { - man.Debug = true - } - return man.Update() -} diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index fba560ee8..035c384d3 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -165,11 +165,11 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { } out := bytes.NewBuffer(nil) - o := &dependencyUpdateOptions{} + o := &refUpdateOptions{} o.helmhome = hh o.chartpath = hh.Path(chartname) - if err := o.run(out); err != nil { + if err := o.run(out, false); err != nil { output := out.String() t.Logf("Output: %s", output) t.Fatal(err) @@ -178,7 +178,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { // Chart repo is down srv.Stop() - if err := o.run(out); err == nil { + if err := o.run(out, false); err == nil { output := out.String() t.Logf("Output: %s", output) t.Fatal("Expected error, got nil") diff --git a/cmd/helm/install.go b/cmd/helm/install.go index a0b23a829..132b3f0a8 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -245,7 +245,7 @@ func (o *installOptions) run(out io.Writer) error { SkipUpdate: false, Getters: getter.All(settings), } - if err := man.Update(); err != nil { + if err := man.Update(false); err != nil { return err } } else { diff --git a/cmd/helm/library.go b/cmd/helm/library.go new file mode 100644 index 000000000..4cb510af4 --- /dev/null +++ b/cmd/helm/library.go @@ -0,0 +1,83 @@ +/* +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 ( + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/require" +) + +const libraryDesc = ` +Manage the libraries of a chart. + +Helm charts store their libraries in 'library/'. For chart developers, it is +often easier to manage libraries in 'Chart.yaml' which declares all +libraries. + +The library commands operate on that file, making it easy to synchronize +between the desired libraries and the actual libraries stored in the +'library/' directory. + +For example, this Chart.yaml declares one library: + + # Chart.yaml + libraries: + - name: common + version: "^2.1.0" + repository: http://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' can be an alias. The alias must start +with 'alias:' or '@'. + +Starting from 2.2.0, repository can be defined as the path to the directory of +the library charts stored locally. The path should start with a prefix of +"file://". For example, + + # Chart.yaml + libraries: + - name: common + version: "^2.1.0" + repository: "file://../library_chart/common" + +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. +` + +func newLibraryCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "library update", + Aliases: []string{"lib", "libraries"}, + Short: "manage a chart's libraries", + Long: libraryDesc, + Args: require.NoArgs, + } + + cmd.AddCommand(newLibraryUpdateCmd(out)) + + return cmd +} diff --git a/cmd/helm/library_update.go b/cmd/helm/library_update.go new file mode 100644 index 000000000..84dd1c8e4 --- /dev/null +++ b/cmd/helm/library_update.go @@ -0,0 +1,69 @@ +/* +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 ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + + "k8s.io/helm/cmd/helm/require" +) + +const libraryUpDesc = ` +Update the on-disk libraries to mirror Chart.yaml. + +This command verifies that the required charts, as expressed in 'Chart.yaml', +are present in 'library/' and are at an acceptable version. It will pull down +the latest charts that satisfy the libraries, and clean up old libraries. + +On successful update, this will generate a lock file that can be used to +rebuild the libraries to an exact version. + +Libraries are not required to be represented in 'Chart.yaml'. For that +reason, an update command will not remove charts unless they are (a) present +in the Chart.yaml file, but (b) at the wrong version. +` + +// newLibraryUpdateCmd creates a new library update command. +func newLibraryUpdateCmd(out io.Writer) *cobra.Command { + o := &refUpdateOptions{ + chartpath: ".", + } + + cmd := &cobra.Command{ + Use: "update CHART", + Aliases: []string{"up"}, + Short: "update library/ based on the contents of Chart.yaml", + Long: libraryUpDesc, + Args: require.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + o.chartpath = filepath.Clean(args[0]) + } + o.helmhome = settings.Home + return o.run(out, true) + }, + } + + f := cmd.Flags() + f.BoolVar(&o.verify, "verify", false, "verify the packages against signatures") + f.StringVar(&o.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + + return cmd +} diff --git a/cmd/helm/package.go b/cmd/helm/package.go index c5ec6e36a..d7dc97ce3 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -52,6 +52,7 @@ Versioned chart archives are used by Helm package repositories. type packageOptions struct { appVersion string // --app-version dependencyUpdate bool // --dependency-update + libraryUpdate bool // --library-update destination string // --destination key string // --key keyring string // --keyring @@ -103,6 +104,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { f.StringVar(&o.appVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVarP(&o.destination, "destination", "d", ".", "location to write the chart.") f.BoolVarP(&o.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) + f.BoolVarP(&o.libraryUpdate, "library-update", "l", false, `update libraries from "Chart.yaml" to dir "library/" before packaging`) o.valuesOptions.addFlags(f) return cmd @@ -124,7 +126,22 @@ func (o *packageOptions) run(out io.Writer) error { Debug: settings.Debug, } - if err := downloadManager.Update(); err != nil { + if err := downloadManager.Update(false); err != nil { + return err + } + } + + if o.libraryUpdate { + downloadManager := &downloader.Manager{ + Out: out, + ChartPath: path, + HelmHome: settings.Home, + Keyring: o.keyring, + Getters: getter.All(settings), + Debug: settings.Debug, + } + + if err := downloadManager.Update(true); err != nil { return err } } diff --git a/cmd/helm/reference_update.go b/cmd/helm/reference_update.go new file mode 100644 index 000000000..eb2fd7a55 --- /dev/null +++ b/cmd/helm/reference_update.go @@ -0,0 +1,55 @@ +/* +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 ( + "io" + + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/helmpath" +) + +// refUpdateOptions describes a 'helm library/dependency update' +type refUpdateOptions struct { + keyring string // --keyring + skipRefresh bool // --skip-refresh + verify bool // --verify + + // args + chartpath string + + helmhome helmpath.Home +} + +// run runs the full library update process. +func (o *refUpdateOptions) run(out io.Writer, lib bool) error { + man := &downloader.Manager{ + Out: out, + ChartPath: o.chartpath, + HelmHome: o.helmhome, + Keyring: o.keyring, + SkipUpdate: o.skipRefresh, + Getters: getter.All(settings), + } + if o.verify { + man.Verify = downloader.VerifyAlways + } + if settings.Debug { + man.Debug = true + } + return man.Update(lib) +} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index a45f1d58d..5c970a751 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -65,6 +65,7 @@ func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Wri // chart commands newCreateCmd(out), newDependencyCmd(out), + newLibraryCmd(out), newPullCmd(out), newShowCmd(out), newLintCmd(out), diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index ac2bf8b6b..c06aa9ab3 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -19,7 +19,8 @@ package chart const APIVersionv1 = "v1" // Chart is a helm package that contains metadata, a default config, zero or more -// optionally parameterizable templates, and zero or more charts (dependencies). +// optionally parameterizable templates, and zero or more +// charts (dependencies/libraries). type Chart struct { // Metadata is the contents of the Chartfile. Metadata *Metadata @@ -37,6 +38,7 @@ type Chart struct { parent *Chart dependencies []*Chart + libraries []*Chart } // SetDependencies replaces the chart dependencies. @@ -72,6 +74,9 @@ func (ch *Chart) Root() *Chart { // Dependencies are the charts that this chart depends on. func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } +// Libraries are the common charts that this chart uses. +func (ch *Chart) Libraries() []*Chart { return ch.libraries } + // IsRoot determines if the chart is the root chart. func (ch *Chart) IsRoot() bool { return ch.parent == nil } diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go index 0837f8d19..1da3bd9e4 100644 --- a/pkg/chart/dependency.go +++ b/pkg/chart/dependency.go @@ -19,7 +19,7 @@ import "time" // Dependency describes a chart upon which another chart depends. // -// Dependencies can be used to express developer intent, or to capture the state +// Dependencies/libraries 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. @@ -49,9 +49,9 @@ type Dependency struct { Alias string `json:"alias,omitempty"` } -// Lock is a lock file for dependencies. +// Lock is a lock file for dependencies/libraries. // -// It represents the state that the dependencies should be in. +// It represents the state that the dependencies/libraries should be in. type Lock struct { // Genderated is the date the lock file was last generated. Generated time.Time `json:"generated"` @@ -59,4 +59,6 @@ type Lock struct { Digest string `json:"digest"` // Dependencies is the list of dependencies that this lock file has locked. Dependencies []*Dependency `json:"dependencies"` + // Libraries is the list of libraries that this lock file has locked. + Libraries []*Dependency `json:"libraries"` } diff --git a/pkg/chart/metadata.go b/pkg/chart/metadata.go index 8541aa965..9d626dc08 100644 --- a/pkg/chart/metadata.go +++ b/pkg/chart/metadata.go @@ -64,4 +64,6 @@ type Metadata struct { KubeVersion string `json:"kubeVersion,omitempty"` // Dependencies are a list of dependencies for a chart. Dependencies []*Dependency `json:"dependencies,omitempty"` + // Libraries are a list of libraries for a chart. + Libraries []*Dependency `json:"libraries,omitempty"` } diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 17d04ffdd..2dfce690e 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -40,7 +40,7 @@ import ( "k8s.io/helm/pkg/urlutil" ) -// Manager handles the lifecycle of fetching, resolving, and storing dependencies. +// Manager handles the lifecycle of fetching, resolving, and storing dependencies/libraries. type Manager struct { // Out is used to print warnings and notifications. Out io.Writer @@ -75,7 +75,7 @@ func (m *Manager) Build() error { // an update. lock := c.Lock if lock == nil { - return m.Update() + return m.Update(false) } req := c.Metadata.Dependencies @@ -96,7 +96,7 @@ func (m *Manager) Build() error { } // Now we need to fetch every package here into charts/ - if err := m.downloadAll(lock.Dependencies); err != nil { + if err := m.downloadAll(lock.Dependencies, false); err != nil { return err } @@ -108,20 +108,25 @@ func (m *Manager) Build() error { // It first reads the Chart.yaml file, and then attempts to // negotiate versions based on that. It will download the versions // from remote chart repositories unless SkipUpdate is true. -func (m *Manager) Update() error { +func (m *Manager) Update(isLib bool) error { c, err := m.loadChartDir() if err != nil { return err } - // If no dependencies are found, we consider this a successful + // If no dependencies/libraries are found, we consider this a successful // completion. - req := c.Metadata.Dependencies + var req []*chart.Dependency + if isLib { + req = c.Metadata.Libraries + } else { + req = c.Metadata.Dependencies + } if req == nil { return nil } - // Hash dependencies + // Hash dependencies/libraries // FIXME should this hash all of Chart.yaml hash, err := resolver.HashReq(req) if err != nil { @@ -143,14 +148,20 @@ func (m *Manager) Update() error { } // Now we need to find out which version of a chart best satisfies the - // dependencies in the Chart.yaml - lock, err := m.resolve(req, repoNames, hash) + // dependencies/libraries in the Chart.yaml + lock, err := m.resolve(req, repoNames, hash, isLib) if err != nil { return err } // Now we need to fetch every package here into charts/ - if err := m.downloadAll(lock.Dependencies); err != nil { + var lockReq []*chart.Dependency + if isLib { + lockReq = lock.Libraries + } else { + lockReq = lock.Dependencies + } + if err := m.downloadAll(lockReq, isLib); err != nil { return err } @@ -173,26 +184,35 @@ func (m *Manager) loadChartDir() (*chart.Chart, error) { return loader.LoadDir(m.ChartPath) } -// resolve takes a list of dependencies and translates them into an exact version to download. +// resolve takes a list of dependencies/libraries and translates them into an exact version to download. // -// This returns a lock file, which has all of the dependencies normalized to a specific version. -func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string, hash string) (*chart.Lock, error) { +// This returns a lock file, which has all of the dependencies/libraries normalized to a specific version. +func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string, hash string, isLib bool) (*chart.Lock, error) { res := resolver.New(m.ChartPath, m.HelmHome) - return res.Resolve(req, repoNames, hash) + return res.Resolve(req, repoNames, hash, isLib) } -// downloadAll takes a list of dependencies and downloads them into charts/ +// downloadAll takes a list of dependencies/libraries and downloads them into charts/ // // It will delete versions of the chart that exist on disk and might cause // a conflict. -func (m *Manager) downloadAll(deps []*chart.Dependency) error { +func (m *Manager) downloadAll(deps []*chart.Dependency, isLib bool) error { repos, err := m.loadChartRepositories() if err != nil { return err } - destPath := filepath.Join(m.ChartPath, "charts") - tmpPath := filepath.Join(m.ChartPath, "tmpcharts") + var dirName string + var tmpDirName string + if isLib { + dirName = "library" + tmpDirName = "tmplibrary" + } else { + dirName = "charts" + tmpDirName = "tmpcharts" + } + destPath := filepath.Join(m.ChartPath, dirName) + tmpPath := filepath.Join(m.ChartPath, tmpDirName) // Create 'charts' directory if it doesn't already exist. if fi, err := os.Stat(destPath); err != nil { diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go index d658f5f6f..ca6287ccb 100644 --- a/pkg/resolver/resolver.go +++ b/pkg/resolver/resolver.go @@ -32,7 +32,7 @@ import ( "k8s.io/helm/pkg/repo" ) -// Resolver resolves dependencies from semantic version ranges to a particular version. +// Resolver resolves libraries/dependencies from semantic version ranges to a particular version. type Resolver struct { chartpath string helmhome helmpath.Home @@ -46,10 +46,10 @@ func New(chartpath string, helmhome helmpath.Home) *Resolver { } } -// Resolve resolves dependencies and returns a lock file with the resolution. -func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string, d string) (*chart.Lock, error) { +// Resolve resolves libraries/dependencies and returns a lock file with the resolution. +func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string, d string, isLib bool) (*chart.Lock, error) { - // Now we clone the dependencies, locking as we go. + // Now we clone the libraries/dependencies, locking as we go. locked := make([]*chart.Dependency, len(reqs)) missing := []string{} for i, d := range reqs { @@ -68,7 +68,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string } constraint, err := semver.NewConstraint(d.Version) if err != nil { - return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name) + return nil, errors.Wrapf(err, "dependency/library %q has an invalid version/constraint format", d.Name) } repoIndex, err := repo.LoadIndexFile(r.helmhome.CacheIndex(repoNames[d.Name])) @@ -107,6 +107,15 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string if len(missing) > 0 { return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", ")) } + + if isLib { + return &chart.Lock{ + Generated: time.Now(), + Digest: d, + Libraries: locked, + }, nil + } + return &chart.Lock{ Generated: time.Now(), Digest: d, @@ -114,7 +123,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string }, nil } -// HashReq generates a hash of the dependencies. +// HashReq generates a hash of the libraries/dependencies. // // This should be used only to compare against another hash generated by this // function. @@ -128,7 +137,7 @@ func HashReq(req []*chart.Dependency) (string, error) { } // GetLocalPath generates absolute local path when use -// "file://" in repository of dependencies +// "file://" in repository of libraries/dependencies func GetLocalPath(repo, chartpath string) (string, error) { var depPath string var err error diff --git a/pkg/resolver/resolver_test.go b/pkg/resolver/resolver_test.go index 7ec0cd387..8a4c0239a 100644 --- a/pkg/resolver/resolver_test.go +++ b/pkg/resolver/resolver_test.go @@ -95,7 +95,7 @@ func TestResolve(t *testing.T) { t.Fatal(err) } - l, err := r.Resolve(tt.req, repoNames, hash) + l, err := r.Resolve(tt.req, repoNames, hash, false) if err != nil { if tt.err { continue