pull/30855/merge
Alik Khilazhev 2 days ago committed by GitHub
commit 07fc071bbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

7
.gitignore vendored

@ -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

@ -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.

@ -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

@ -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

@ -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

@ -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")
}

@ -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())
}

@ -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)
}
}

@ -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)
},
}

@ -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
}

@ -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
}
}
}

@ -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",

@ -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")

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: dep1

@ -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"

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: dep2

@ -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

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: root

Binary file not shown.

@ -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.

@ -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.

@ -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)
}
}

Loading…
Cancel
Save