From 42ef0fe2ff18935ee9600de700b1ac0a6f54850e Mon Sep 17 00:00:00 2001 From: Paul Gier Date: Mon, 19 Sep 2022 10:07:44 -0500 Subject: [PATCH] issue-11308: allow individual dependency updates This adds a new option to `helm dependency update` called `--dependency` which allows the user to select one or more dependencies from Chart.yaml to be updated while leaving the remaining dependencies set at their current values in Chart.lock. Signed-off-by: Paul Gier --- cmd/helm/dependency_update.go | 8 +++++ pkg/action/dependency.go | 9 ++--- pkg/downloader/manager.go | 65 ++++++++++++++++++++++++++++++++-- pkg/downloader/manager_test.go | 41 +++++++++++++++++++++ 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index ad0188f17..8f7b59a92 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -40,6 +40,12 @@ rebuild the dependencies to an exact version. Dependencies 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. + +By default all dependencies will be updated to the latest version in the specified +range. For projects with an existing 'Chart.lock' file, one or more dependencies +can be selected for update without affecting other dependencies by using the +'-d/--dependency' option. Only the selected dependencies will be updated in +'Chart.lock' and the remaining dependencies will be unaffected. ` // newDependencyUpdateCmd creates a new dependency update command. @@ -62,6 +68,7 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com ChartPath: chartpath, Keyring: client.Keyring, SkipUpdate: client.SkipRefresh, + Dependencies: client.SelectedDependencies, Getters: getter.All(settings), RegistryClient: cfg.RegistryClient, RepositoryConfig: settings.RepositoryConfig, @@ -79,6 +86,7 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + f.StringSliceVarP(&client.SelectedDependencies, "dependency", "d", []string{}, "update a single dependency by name, requires an existing Chart.lock file") return cmd } diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 3265f1f17..2cd7bc311 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -34,10 +34,11 @@ import ( // // It provides the implementation of 'helm dependency' and its respective subcommands. type Dependency struct { - Verify bool - Keyring string - SkipRefresh bool - ColumnWidth uint + Verify bool + Keyring string + SkipRefresh bool + ColumnWidth uint + SelectedDependencies []string } // NewDependency creates a new Dependency object with the given configuration. diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 18b28dde1..cfe7d1f73 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -71,6 +71,8 @@ type Manager struct { Keyring string // SkipUpdate indicates that the repository should not be updated first. SkipUpdate bool + // Dependencies is a list of specific dependencies to update instead of updating all. + Dependencies []string // Getter collection for the operation Getters []getter.Provider RegistryClient *registry.Client @@ -124,7 +126,7 @@ func (m *Manager) Build() error { if v2Sum != lock.Digest { return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") } - } else { + } else if len(m.Dependencies) == 0 { return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies") } } @@ -151,6 +153,7 @@ func (m *Manager) Build() error { // negotiate versions based on that. It will download the versions // from remote chart repositories unless SkipUpdate is true. func (m *Manager) Update() error { + c, err := m.loadChartDir() if err != nil { return err @@ -190,9 +193,16 @@ func (m *Manager) Update() error { } } + // If the --dependency option is set, we want to keep the existing contents + // of Chart.lock and only upgrade the dependencies specified by the user. + depsToResolve, err := dependenciesToResolve(req, c.Lock, m.Dependencies) + if err != nil { + return err + } + // 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) + lock, err := m.resolve(depsToResolve, repoNames) if err != nil { return err } @@ -896,3 +906,54 @@ func key(name string) (string, error) { } return hex.EncodeToString(hash.Sum(nil)), nil } + +// dependenciesToResolve takes the requested (req) dependencies from Chart.yaml and +// overrides the version with the matching dependency from Chart.lock, except for those +// dependencies which have been selected for update by the user. +// The intention is to allow a user to update only selected dependencies in Chart.yaml +// If no selected deps are specified, returns the original request to update all dependencies. +// Returns an error if lock is nil or if some any unselected dependencies are missing +// from Chart.lock. +func dependenciesToResolve(req []*chart.Dependency, lock *chart.Lock, selected []string) ([]*chart.Dependency, error) { + if len(selected) == 0 { + return req, nil + } + if lock == nil { + return nil, fmt.Errorf("file Chart.lock not found, the '-d/--dependency' requires an existing Chart.lock file") + } + depsToUpdate := []*chart.Dependency{} + for _, dep := range req { + if stringSliceContains(selected, dep.Name) { + depsToUpdate = append(depsToUpdate, dep) + } else { + lockedDep := getDependencyByName(lock.Dependencies, dep.Name) + if lockedDep == nil { + return nil, fmt.Errorf("dependency '%s' missing from Chart.lock, this dependency must be selected with '-d/--dependency' in order to update Chart.lock", dep.Name) + } + depsToUpdate = append(depsToUpdate, lockedDep) + } + } + return depsToUpdate, nil +} + +// stringSliceContains returns true if the slice contains the given string, +// false otherwise. +func stringSliceContains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} + +// getDependencyByName returns the named dependency from the given list, +// or nil if no matching dependency is found. +func getDependencyByName(deps []*chart.Dependency, name string) *chart.Dependency { + for _, dep := range deps { + if dep.Name == name { + return dep + } + } + return nil +} diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index f7ab1a568..c4af5c7a4 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -572,3 +572,44 @@ func TestKey(t *testing.T) { } } } + +func TestGetDependenciesToResolve(t *testing.T) { + chartReq := []*chart.Dependency{ + { + Name: "foo", + Version: "1.x", + }, + { + Name: "bar", + Version: "2.1.x", + }, + } + chartLock := &chart.Lock{ + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Version: "1.5", + }, + { + Name: "bar", + Version: "2.1.2", + }, + }, + } + selectedDeps := []string{ + "foo", + } + depsToResolve, err := dependenciesToResolve(chartReq, chartLock, selectedDeps) + if err != nil { + t.Errorf("failed to get dependency list with error: %v", err) + } + if len(depsToResolve) != 2 { + t.Errorf("incorrect number of deps expected 2, got %v", depsToResolve) + } + if depsToResolve[0].Version != "1.x" { + t.Errorf("incorrect version set for dependency '%s', expected 1.x, got %s", depsToResolve[0].Name, depsToResolve[0].Version) + } + if depsToResolve[1].Version != "2.1.2" { + t.Errorf("incorrect version set for dependency '%s', expected 2.1.2, got %s", depsToResolve[1].Name, depsToResolve[1].Version) + } +}