diff --git a/.gitignore b/.gitignore index 0fd2c6bda..9ad397ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,11 @@ _dist_versions/ bin/ vendor/ # Ignores charts pulled for dependency build tests -cmd/helm/testdata/testcharts/issue-7233/charts/* pkg/cmd/testdata/testcharts/issue-7233/charts/* +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/charts/* +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/Chart.lock +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/charts/* +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/Chart.lock +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/charts/* +pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/Chart.lock .pre-commit-config.yaml diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 03c370c8e..1d16bcda9 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -34,17 +34,18 @@ import ( // // It provides the implementation of 'helm dependency' and its respective subcommands. type Dependency struct { - Verify bool - Keyring string - SkipRefresh bool - ColumnWidth uint - Username string - Password string - CertFile string - KeyFile string - CaFile string - InsecureSkipTLSverify bool - PlainHTTP bool + Verify bool + Keyring string + SkipRefresh bool + ColumnWidth uint + Username string + Password string + CertFile string + KeyFile string + CaFile string + InsecureSkipTLSverify bool + PlainHTTP bool + BuildOrUpdateRecursive bool } // NewDependency creates a new Dependency object with the given configuration. diff --git a/pkg/action/install.go b/pkg/action/install.go index c6d4f723c..e8c03db3d 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -91,20 +91,21 @@ type Install struct { DryRunOption string // HideSecret can be set to true when DryRun is enabled in order to hide // Kubernetes Secrets in the output. It cannot be used outside of DryRun. - HideSecret bool - DisableHooks bool - Replace bool - WaitStrategy kube.WaitStrategy - WaitForJobs bool - Devel bool - DependencyUpdate bool - Timeout time.Duration - Namespace string - ReleaseName string - GenerateName bool - NameTemplate string - Description string - OutputDir string + HideSecret bool + DisableHooks bool + Replace bool + WaitStrategy kube.WaitStrategy + WaitForJobs bool + Devel bool + DependencyUpdate bool + DependencyUpdateRecursive bool + Timeout time.Duration + Namespace string + ReleaseName string + GenerateName bool + NameTemplate string + Description string + OutputDir string // RollbackOnFailure enables rolling back (uninstalling) the release on failure if set RollbackOnFailure bool SkipCRDs bool diff --git a/pkg/action/package.go b/pkg/action/package.go index 92a9a8cb6..9804380d4 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -39,15 +39,16 @@ import ( // // It provides the implementation of 'helm package'. type Package struct { - Sign bool - Key string - Keyring string - PassphraseFile string - cachedPassphrase []byte - Version string - AppVersion string - Destination string - DependencyUpdate bool + Sign bool + Key string + Keyring string + PassphraseFile string + cachedPassphrase []byte + Version string + AppVersion string + Destination string + DependencyUpdate bool + DependencyUpdateRecursive bool RepositoryConfig string RepositoryCache string diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3c84570b2..e41437a9d 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -122,6 +122,8 @@ type Upgrade struct { DisableOpenAPIValidation bool // Get missing dependencies DependencyUpdate bool + // Get missing dependencies, recursively + DependencyUpdateRecursive bool // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex // Enable DNS lookups when rendering templates diff --git a/pkg/cmd/dependency.go b/pkg/cmd/dependency.go index 34bbff6be..b3fc00552 100644 --- a/pkg/cmd/dependency.go +++ b/pkg/cmd/dependency.go @@ -133,4 +133,5 @@ func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) { f.BoolVar(&client.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&client.BuildOrUpdateRecursive, "recursive", false, "build or update dependencies recursively") } diff --git a/pkg/cmd/dependency_build.go b/pkg/cmd/dependency_build.go index 320fe12ae..d82872e49 100644 --- a/pkg/cmd/dependency_build.go +++ b/pkg/cmd/dependency_build.go @@ -75,7 +75,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { if client.Verify { man.Verify = downloader.VerifyIfPossible } - err = man.Build() + err = man.Build(client.BuildOrUpdateRecursive) if e, ok := err.(downloader.ErrRepoNotFound); ok { return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) } diff --git a/pkg/cmd/dependency_build_test.go b/pkg/cmd/dependency_build_test.go index a3473301d..9d031457a 100644 --- a/pkg/cmd/dependency_build_test.go +++ b/pkg/cmd/dependency_build_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/provenance" "helm.sh/helm/v4/pkg/repo/v1" @@ -162,3 +163,64 @@ func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) { t.Fatal(err) } } + +// 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 createTestingChartWithRecursion(t *testing.T, dest, name, baseURL string) { + t.Helper() + cfile := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV2, + Name: name, + Version: "1.2.3", + Dependencies: []*chart.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, + {Name: "compressedchart", Version: "0.1.0", Repository: baseURL}, + {Name: "root", Version: "0.1.0", Repository: baseURL}, + }, + }, + } + if err := chartutil.SaveDir(cfile, dest); err != nil { + t.Fatal(err) + } +} + +func TestDependencyBuildCmdRecursive(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"), + ) + defer srv.Stop() + rootDir := srv.Root() + + srv.LinkIndices() + ociSrv, err := repotest.NewOCIServer(t, rootDir) + if err != nil { + t.Fatal(err) + } + + dir := func(p ...string) string { + return filepath.Join(append([]string{rootDir}, p...)...) + } + + ociChartName := "oci-depending-chart" + c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) + if _, err := chartutil.Save(c, ociSrv.Dir); err != nil { + t.Fatal(err) + } + ociSrv.Run(t, repotest.WithDependingChart(c)) + + chartname := "chart-with-multi-level-deps" + createTestingChartWithRecursion(t, dir(), chartname, srv.URL()) + repoFile := filepath.Join(dir(), "repositories.yaml") + + cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --plain-http --recursive", filepath.Join(rootDir, chartname), repoFile, rootDir) + _, out, err := executeActionCommand(cmd) + + // In the first pass, we basically want the same results as an update. + if err != nil { + t.Logf("Output: %s", out) + t.Fatal(err) + } +} diff --git a/pkg/cmd/dependency_update.go b/pkg/cmd/dependency_update.go index b534fb48a..0a8cd2dfa 100644 --- a/pkg/cmd/dependency_update.go +++ b/pkg/cmd/dependency_update.go @@ -79,7 +79,7 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma if client.Verify { man.Verify = downloader.VerifyAlways } - return man.Update() + return man.Update(client.BuildOrUpdateRecursive) }, } diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 96bf6434b..5432ff2de 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -18,8 +18,10 @@ package cmd import ( "bytes" + "fmt" "io" "os" + "path/filepath" "strings" "testing" "time" @@ -30,6 +32,7 @@ import ( "helm.sh/helm/v4/internal/test" "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/cli" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -56,6 +59,15 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { t.Fatal(err) } } + + if tt.preCmd != nil { + t.Logf("running preCmd (attempt %d): %s", i+1, tt.cmd) + if err := tt.preCmd(t); err != nil { + t.Errorf("expected no error executing preCmd, got: '%v'", err) + t.FailNow() + } + } + t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) _, out, err := executeActionCommandC(storage, tt.cmd) if tt.wantError && err == nil { @@ -134,6 +146,7 @@ type cmdTestCase struct { // Number of repeats (in case a feature was previously flaky and the test checks // it's now stably producing identical results). 0 means test is run exactly once. repeat int + preCmd func(t *testing.T) error } func executeActionCommand(cmd string) (*cobra.Command, string, error) { @@ -151,3 +164,55 @@ func resetEnv() func() { settings = cli.New() } } + +func testChdir(t *testing.T, dir string) func() { + t.Helper() + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + return func() { os.Chdir(old) } +} + +// resetChartDependencyState completely resets dependency state of a given chart +// by deleting `Chart.lock` and `charts/`. +// +// If `recursive` is set to true, it will recurse into all local dependency charts +// and do the same. +func resetChartDependencyState(chartPath string, recursive bool) error { + chartRequested, err := loader.Load(chartPath) + + if err != nil { + return err + } + + os.Remove(fmt.Sprintf("%s/Chart.lock", chartPath)) + os.RemoveAll(fmt.Sprintf("%s/charts/", chartPath)) + + if recursive { + for _, chartDep := range chartRequested.Metadata.Dependencies { + if strings.HasPrefix( + chartDep.Repository, + "file://", + ) { + + fullDepPath, err := filepath.Abs( + fmt.Sprintf("%s/%s", chartPath, chartDep.Repository[7:]), + ) + + if err != nil { + return err + } + + if err := resetChartDependencyState(fullDepPath, recursive); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 4f30bd7df..240d60868 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -207,6 +207,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.StringVar(&client.Description, "description", "", "add a custom description") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") + f.BoolVar(&client.DependencyUpdateRecursive, "dependency-update-recursive", false, "update dependencies recursively if they are missing before installing the chart") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema") f.BoolVar(&client.RollbackOnFailure, "rollback-on-failure", false, "if set, Helm will rollback (uninstall) the installation upon failure. The --wait flag will be default to \"watcher\" if --rollback-on-failure is set") f.MarkDeprecated("atomic", "use --rollback-on-failure instead") @@ -288,7 +289,8 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options // As of Helm 2.4.0, this is treated as a stopping condition: // https://github.com/helm/helm/issues/2209 if err := action.CheckDependencies(chartRequested, req); err != nil { - if client.DependencyUpdate { + err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err) + if client.DependencyUpdate || client.DependencyUpdateRecursive { man := &downloader.Manager{ Out: out, ChartPath: cp, @@ -301,7 +303,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options Debug: settings.Debug, RegistryClient: client.GetRegistryClient(), } - if err := man.Update(); err != nil { + if err := man.Update(client.DependencyUpdateRecursive); err != nil { return nil, err } // Reload the chart with the updated Chart.lock file. @@ -309,7 +311,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, fmt.Errorf("failed reloading chart after repo update: %w", err) } } else { - return nil, fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err) + return nil, err } } } diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index f0f12e4f7..2547ba96a 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -153,6 +153,12 @@ func TestInstall(t *testing.T) { cmd: "install --dependency-update updeps testdata/testcharts/chart-with-subchart-update", golden: "output/chart-with-subchart-update.txt", }, + // Install chart with update-dependency-recursive + { + name: "install chart with dependency update recursive", + cmd: "install --dependency-update-recursive recdeps testdata/testcharts/chart-with-multi-level-deps/root/", + golden: "output/install-dependency-update-recursive.txt", + }, // Install, chart with bad dependencies in Chart.yaml in /charts { name: "install chart with bad dependencies in Chart.yaml", diff --git a/pkg/cmd/package.go b/pkg/cmd/package.go index fc56e936a..5ca6d2d39 100644 --- a/pkg/cmd/package.go +++ b/pkg/cmd/package.go @@ -103,7 +103,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { ContentCache: settings.ContentCache, } - if err := downloadManager.Update(); err != nil { + if err := downloadManager.Update(client.DependencyUpdateRecursive); err != nil { return err } } @@ -126,6 +126,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) + f.BoolVarP(&client.DependencyUpdateRecursive, "dependency-update-recursive", "r", false, `update dependencies recursively from from "Chart.yaml" and all of its subcharts before packaging`) f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart") f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart") f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 5bcccf5d0..74d348741 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -138,6 +138,22 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), golden: "output/template-skip-tests.txt", }, + { + name: "template with dependency update recursive", + preCmd: func(_ *testing.T) error { + // We must reset the chart's dependency to actually + // exercise the ability to provision missing nested depencendies. + // If we don't do this, the chart will contain the `tgz` files from previous runs + // and the `--dependency-update-recursive` flag won't do much. + // Note the dependency files for the chart are ignored via .gitignore. + return resetChartDependencyState( + "testdata/testcharts/chart-with-multi-level-deps/root", + true, + ) + }, + cmd: fmt.Sprintf(`template '%s' --dependency-update-recursive`, "testdata/testcharts/chart-with-multi-level-deps/root"), + golden: "output/template-dependency-update-recursive.txt", + }, { // This test case is to ensure the case where specified dependencies // in the Chart.yaml and those where the Chart.yaml don't have them diff --git a/pkg/cmd/testdata/output/install-dependency-update-recursive.txt b/pkg/cmd/testdata/output/install-dependency-update-recursive.txt new file mode 100644 index 000000000..16ece824e --- /dev/null +++ b/pkg/cmd/testdata/output/install-dependency-update-recursive.txt @@ -0,0 +1,7 @@ +NAME: recdeps +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +DESCRIPTION: Install complete +TEST SUITE: None diff --git a/pkg/cmd/testdata/output/template-dependency-update-recursive.txt b/pkg/cmd/testdata/output/template-dependency-update-recursive.txt new file mode 100644 index 000000000..5c451ad75 --- /dev/null +++ b/pkg/cmd/testdata/output/template-dependency-update-recursive.txt @@ -0,0 +1,16 @@ +Saving 1 charts +Deleting outdated charts +Saving 1 charts +Deleting outdated charts +--- +# Source: root/charts/dep1/templates/configmap1.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dep1 +--- +# Source: root/templates/configmaproot.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: root diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/Chart.yaml new file mode 100644 index 000000000..2919254ca --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: dep1 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +dependencies: + - name: dep2 + repository: file://../dep2 + version: 0.1.0 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/templates/configmap1.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/templates/configmap1.yaml new file mode 100644 index 000000000..e80109f69 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/templates/configmap1.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dep1 diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep1/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/Chart.yaml new file mode 100644 index 000000000..ec045301e --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: dep2 +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/templates/configmap2.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/templates/configmap2.yaml new file mode 100644 index 000000000..899e7f6ba --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/templates/configmap2.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dep2 diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/dep2/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/Chart.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/Chart.yaml new file mode 100644 index 000000000..241e9ed0e --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: root +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +dependencies: + - name: dep1 + repository: file://../dep1 + version: 0.1.0 \ No newline at end of file diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/templates/configmaproot.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/templates/configmaproot.yaml new file mode 100644 index 000000000..2caac7cce --- /dev/null +++ b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/templates/configmaproot.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: root diff --git a/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/values.yaml b/pkg/cmd/testdata/testcharts/chart-with-multi-level-deps/root/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/cmd/testdata/testcharts/root-0.1.0.tgz b/pkg/cmd/testdata/testcharts/root-0.1.0.tgz new file mode 100644 index 000000000..265ce8d89 Binary files /dev/null and b/pkg/cmd/testdata/testcharts/root-0.1.0.tgz differ diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index fcc4f9294..8e0ba9335 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -149,6 +149,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.SkipSchemaValidation = client.SkipSchemaValidation instClient.Description = client.Description instClient.DependencyUpdate = client.DependencyUpdate + instClient.DependencyUpdateRecursive = client.DependencyUpdateRecursive instClient.Labels = client.Labels instClient.EnableDNS = client.EnableDNS instClient.HideSecret = client.HideSecret @@ -219,7 +220,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ContentCache: settings.ContentCache, Debug: settings.Debug, } - if err := man.Update(); err != nil { + if err := man.Update(client.DependencyUpdateRecursive); err != nil { return err } // Reload the chart with the updated Chart.lock file. diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index d41b8fdb4..7c4edb823 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -23,6 +23,7 @@ import ( "io" stdfs "io/fs" "log" + "log/slog" "net/url" "os" "path/filepath" @@ -85,8 +86,8 @@ type Manager struct { // If the lockfile is not present, this will run a Manager.Update() // // If SkipUpdate is set, this will not update the repository. -func (m *Manager) Build() error { - c, err := m.loadChartDir() +func (m *Manager) Build(recursive bool) error { + c, err := m.loadChartDir(m.ChartPath) if err != nil { return err } @@ -95,7 +96,7 @@ func (m *Manager) Build() error { // an update. lock := c.Lock if lock == nil { - return m.Update() + return m.Update(recursive) } // Check that all of the repos we're dependent on actually exist. @@ -152,8 +153,31 @@ 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 { - c, err := m.loadChartDir() +// +// If `recursive` is set to true, it will iterate over all dependencies +// recursively and perform the update. +func (m *Manager) Update(recursive bool) error { + if recursive { + depChartPaths, err := m.locateLocalDependencies(m.ChartPath, recursive) + + if err != nil { + return err + } + + for _, depChartPath := range depChartPaths { + if err := m.doUpdate(depChartPath); err != nil { + return err + } + } + } + + // for last, update the root chart + return m.doUpdate(m.ChartPath) +} + +func (m *Manager) doUpdate(chartPath string) error { + slog.Debug("update chart dependencies", "chartPath", chartPath) + c, err := m.loadChartDir(chartPath) if err != nil { return err } @@ -221,13 +245,14 @@ func (m *Manager) Update() error { return writeLock(m.ChartPath, lock, c.Metadata.APIVersion == chart.APIVersionV1) } -func (m *Manager) loadChartDir() (*chart.Chart, error) { - if fi, err := os.Stat(m.ChartPath); err != nil { - return nil, fmt.Errorf("could not find %s: %w", m.ChartPath, err) +func (m *Manager) loadChartDir(chartPath string) (*chart.Chart, error) { + slog.Debug("loading chart directory", "chartPath", chartPath) + if fi, err := os.Stat(chartPath); err != nil { + return nil, fmt.Errorf("could not find %s: %w", chartPath, err) } else if !fi.IsDir() { return nil, errors.New("only unpacked charts can be updated") } - return loader.LoadDir(m.ChartPath) + return loader.LoadDir(chartPath) } // resolve takes a list of dependencies and translates them into an exact version to download. @@ -771,6 +796,62 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]* return url, username, password, false, false, "", "", "", err } +// locateLocalDependencies locates local dependencies for the given chart (optionally recursively) +// +// The returned list of Chart paths is ordered from "leaf to root" so we can issue updates in +// the right order when iterating over this list. +func (m *Manager) locateLocalDependencies(baseChartPath string, resursive bool) ([]string, error) { + slog.Debug("locating local dependencies", "baseChartPath", baseChartPath, "resursive", resursive) + reversedDeps := []string{} + + baseChart, err := m.loadChartDir(baseChartPath) + + if err != nil { + return nil, err + } + + for _, chartDependency := range baseChart.Metadata.Dependencies { + + fullDepChartPath := chartDependency.Repository + + if strings.HasPrefix( + chartDependency.Repository, + "file://", + ) { + + fullDepChartPath, err = filepath.Abs( + fmt.Sprintf( + "%s/%s", + baseChartPath, chartDependency.Repository[7:]), // removes "file://" + ) + + if err != nil { + return nil, err + } + + reversedDeps = append( + []string{fullDepChartPath}, + reversedDeps..., + ) + + if resursive { + subDeps, err := m.locateLocalDependencies(fullDepChartPath, resursive) + + if err != nil { + return nil, err + } + + reversedDeps = append( + subDeps, + reversedDeps..., + ) + } + } + } + + return reversedDeps, nil +} + // findEntryByName finds an entry in the chart repository whose name matches the given name. // // It returns the ChartVersions for that entry. diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index 9e27f183f..b01bbbd94 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -359,11 +359,13 @@ func TestUpdateBeforeBuild(t *testing.T) { } // Update before Build. see issue: https://github.com/helm/helm/issues/7101 - if err := m.Update(); err != nil { + err := m.Update(false) + if err != nil { t.Fatal(err) } - if err := m.Build(); err != nil { + err = m.Build(false) + if err != nil { t.Fatal(err) } } @@ -432,7 +434,8 @@ func TestUpdateWithNoRepo(t *testing.T) { } // Test the update - if err := m.Update(); err != nil { + err := m.Update(false) + if err != nil { t.Fatal(err) } } @@ -499,12 +502,14 @@ func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Depe } // First build will update dependencies and create Chart.lock file. - if err := m.Build(); err != nil { + err := m.Build(false) + if err != nil { t.Fatal(err) } // Second build should be passed. See PR #6655. - if err := m.Build(); err != nil { + err = m.Build(false) + if err != nil { t.Fatal(err) } }