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

@ -45,6 +45,7 @@ type Dependency struct {
CaFile string
InsecureSkipTLSverify bool
PlainHTTP bool
BuildOrUpdateRecursive bool
}
// NewDependency creates a new Dependency object with the given configuration.

@ -98,6 +98,7 @@ type Install struct {
WaitForJobs bool
Devel bool
DependencyUpdate bool
DependencyUpdateRecursive bool
Timeout time.Duration
Namespace string
ReleaseName string

@ -48,6 +48,7 @@ type Package struct {
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