From 8b73a97bdf1825df24a0d394fe6c3f6c1bc97f07 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 08:24:38 +0100 Subject: [PATCH 1/8] feat: add configurable chart size limits via flags and env vars Allow users to configure max chart size and max file size through CLI flags (--max-chart-size, --max-file-size) and environment variables (HELM_MAX_CHART_SIZE, HELM_MAX_FILE_SIZE) to address cases where the security limits introduced in e4da497 are too restrictive for some legitimate charts. Signed-off-by: Benoit Tigeot --- internal/chart/v3/loader/archive.go | 35 +++++-- internal/chart/v3/loader/directory.go | 34 +++++-- internal/chart/v3/loader/load.go | 16 +++- internal/chart/v3/util/expand.go | 18 +++- pkg/action/install.go | 4 + pkg/action/pull.go | 14 ++- pkg/action/upgrade.go | 4 + pkg/chart/loader/archive/archive.go | 44 ++++++--- pkg/chart/loader/load.go | 95 ++++++++++++++++--- pkg/chart/v2/loader/archive.go | 15 ++- pkg/chart/v2/loader/directory.go | 9 +- pkg/chart/v2/loader/load.go | 12 ++- pkg/chart/v2/util/expand.go | 18 +++- pkg/cli/environment.go | 22 +++++ pkg/cli/environment_test.go | 53 +++++++++++ pkg/cmd/install.go | 13 ++- pkg/cmd/install_test.go | 6 ++ pkg/cmd/pull.go | 2 + pkg/cmd/pull_test.go | 6 ++ pkg/cmd/root.go | 2 + pkg/cmd/testdata/output/env-comp.txt | 2 + .../install-with-restricted-chart-size.txt | 1 + .../output/upgrade-failed-max-chart-size.txt | 1 + pkg/cmd/upgrade.go | 15 ++- pkg/cmd/upgrade_test.go | 7 ++ 25 files changed, 386 insertions(+), 62 deletions(-) create mode 100644 pkg/cmd/testdata/output/install-with-restricted-chart-size.txt create mode 100644 pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt diff --git a/internal/chart/v3/loader/archive.go b/internal/chart/v3/loader/archive.go index 358c2ce4d..21ec86782 100644 --- a/internal/chart/v3/loader/archive.go +++ b/internal/chart/v3/loader/archive.go @@ -27,16 +27,34 @@ import ( "helm.sh/helm/v4/pkg/chart/loader/archive" ) -// FileLoader loads a chart from a file -type FileLoader string +// FileLoader loads a chart from a file with embedded options +type FileLoader struct { + path string + opts archive.Options +} // Load loads a chart func (l FileLoader) Load() (*chart.Chart, error) { - return LoadFile(string(l)) + return LoadFile(l.path) +} + +// NewFileLoader creates a file loader with custom options +func NewFileLoader(path string, opts archive.Options) FileLoader { + return FileLoader{path: path, opts: opts} +} + +// NewDefaultFileLoader creates a file loader with default options +func NewDefaultFileLoader(path string) FileLoader { + return FileLoader{path: path, opts: archive.DefaultOptions} } -// LoadFile loads from an archive file. +// LoadFile loads from an archive file with default options func LoadFile(name string) (*chart.Chart, error) { + return LoadFileWithOptions(name, archive.DefaultOptions) +} + +// LoadFile loads from an archive file with the provided options +func LoadFileWithOptions(name string, opts archive.Options) (*chart.Chart, error) { if fi, err := os.Stat(name); err != nil { return nil, err } else if fi.IsDir() { @@ -54,7 +72,7 @@ func LoadFile(name string) (*chart.Chart, error) { return nil, err } - c, err := LoadArchive(raw) + c, err := LoadArchiveWithOptions(raw, opts) if err != nil { if err == gzip.ErrHeader { return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) @@ -65,7 +83,12 @@ func LoadFile(name string) (*chart.Chart, error) { // LoadArchive loads from a reader containing a compressed tar archive. func LoadArchive(in io.Reader) (*chart.Chart, error) { - files, err := archive.LoadArchiveFiles(in) + return LoadArchiveWithOptions(in, archive.DefaultOptions) +} + +// LoadArchive loads from a reader containing a compressed tar archive with the provided options. +func LoadArchiveWithOptions(in io.Reader, opts archive.Options) (*chart.Chart, error) { + files, err := archive.LoadArchiveFilesWithOptions(in, opts) if err != nil { return nil, err } diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index dfe3af3b2..b49874f76 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -32,17 +32,39 @@ import ( var utf8bom = []byte{0xEF, 0xBB, 0xBF} // DirLoader loads a chart from a directory -type DirLoader string +type DirLoader struct { + path string + opts archive.Options +} + +// NewDirLoader creates a new directory loader with default options +func NewDefaultDirLoader(path string) DirLoader { + return DirLoader{path: path, opts: archive.DefaultOptions} +} -// Load loads the chart +// NewDirLoader creates a new directory loader with custom options +func NewDirLoader(path string, opts archive.Options) DirLoader { + return DirLoader{path: path, opts: opts} +} + +// Load loads the chart using default behavior for directories. func (l DirLoader) Load() (*chart.Chart, error) { - return LoadDir(string(l)) + return LoadDir(l.path) +} + +func (l DirLoader) LoadWithOptions() (*chart.Chart, error) { + return LoadDirWithOptions(l.path, l.opts) +} + +// LoadDir loads from a directory with default options +func LoadDir(dir string) (*chart.Chart, error) { + return LoadDirWithOptions(dir, archive.DefaultOptions) } // LoadDir loads from a directory. // // This loads charts only from directories. -func LoadDir(dir string) (*chart.Chart, error) { +func LoadDirWithOptions(dir string, opts archive.Options) (*chart.Chart, error) { topdir, err := filepath.Abs(dir) if err != nil { return nil, err @@ -100,8 +122,8 @@ func LoadDir(dir string) (*chart.Chart, error) { return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) } - if fi.Size() > archive.MaxDecompressedFileSize { - return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) + if fi.Size() > opts.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), opts.MaxDecompressedFileSize) } data, err := os.ReadFile(name) diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 1c5b4cad1..36337a11b 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -41,6 +41,18 @@ type ChartLoader interface { Load() (*chart.Chart, error) } +// LoadWithOptions takes a string name, resolves it to a file or directory, and loads it with custom options. +func LoadWithOptions(name string, opts archive.Options) (*chart.Chart, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return LoadDirWithOptions(name, opts) + } + return LoadFileWithOptions(name, opts) +} + // Loader returns a new ChartLoader appropriate for the given chart name func Loader(name string) (ChartLoader, error) { fi, err := os.Stat(name) @@ -48,9 +60,9 @@ func Loader(name string) (ChartLoader, error) { return nil, err } if fi.IsDir() { - return DirLoader(name), nil + return NewDefaultDirLoader(name), nil } - return FileLoader(name), nil + return NewDefaultFileLoader(name), nil } // Load takes a string name, tries to resolve it to a file or directory, and then loads it. diff --git a/internal/chart/v3/util/expand.go b/internal/chart/v3/util/expand.go index 1a10fce3c..e1a99a0d8 100644 --- a/internal/chart/v3/util/expand.go +++ b/internal/chart/v3/util/expand.go @@ -30,9 +30,14 @@ import ( "helm.sh/helm/v4/pkg/chart/loader/archive" ) -// Expand uncompresses and extracts a chart into the specified directory. +// Expand uncompresses and extracts a chart into the specified directory using default options. func Expand(dir string, r io.Reader) error { - files, err := archive.LoadArchiveFiles(r) + return ExpandWithOptions(dir, r, archive.DefaultOptions) +} + +// ExpandWithOptions uncompresses and extracts a chart into the specified directory using the provided options. +func ExpandWithOptions(dir string, r io.Reader, opts archive.Options) error { + files, err := archive.LoadArchiveFilesWithOptions(r, opts) if err != nil { return err } @@ -83,12 +88,17 @@ func Expand(dir string, r io.Reader) error { return nil } -// ExpandFile expands the src file into the dest directory. +// ExpandFile expands the src file into the dest directory using default options. func ExpandFile(dest, src string) error { + return ExpandFileWithOptions(dest, src, archive.DefaultOptions) +} + +// ExpandFileWithOptions expands the src file into the dest directory using the provided options. +func ExpandFileWithOptions(dest, src string, opts archive.Options) error { h, err := os.Open(src) if err != nil { return err } defer h.Close() - return Expand(dest, h) + return ExpandWithOptions(dest, h, opts) } diff --git a/pkg/action/install.go b/pkg/action/install.go index 9008c06ec..ce376e1ce 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -132,6 +132,10 @@ type Install struct { // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex goroutineCount atomic.Int32 + // MaxChartSize is the maximum size of a decompressed chart in bytes + MaxChartSize int64 + // MaxChartFileSize is the maximum size of a single file in a chart in bytes + MaxChartFileSize int64 } // ChartPathOptions captures common options used for controlling chart paths diff --git a/pkg/action/pull.go b/pkg/action/pull.go index dd051167b..b2f56f4f6 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" + "helm.sh/helm/v4/pkg/chart/loader/archive" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/downloader" @@ -44,6 +45,10 @@ type Pull struct { UntarDir string DestDir string cfg *Configuration + // MaxChartSize is the maximum decompressed size of a chart in bytes + MaxChartSize int64 + // MaxChartFileSize is the maximum size of a single file in a chart in bytes + MaxChartFileSize int64 } type PullOpt func(*Pull) @@ -169,7 +174,14 @@ func (p *Pull) Run(chartRef string) (string, error) { return out.String(), fmt.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck) } - return out.String(), chartutil.ExpandFile(ud, saved) + opts := archive.DefaultOptions + if p.MaxChartSize > 0 { + opts.MaxDecompressedChartSize = p.MaxChartSize + } + if p.MaxChartFileSize > 0 { + opts.MaxDecompressedFileSize = p.MaxChartFileSize + } + return out.String(), chartutil.ExpandFileWithOptions(ud, saved, opts) } return out.String(), nil } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 57a4a0272..c0c47c075 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -129,6 +129,10 @@ type Upgrade struct { EnableDNS bool // TakeOwnership will skip the check for helm annotations and adopt all existing resources. TakeOwnership bool + // MaxChartSize is the maximum decompressed size of a chart in bytes + MaxChartSize int64 + // MaxChartFileSize is the maximum size of a single file in a chart in bytes + MaxChartFileSize int64 } type resultMessage struct { diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go index c6875db3f..032cc7675 100644 --- a/pkg/chart/loader/archive/archive.go +++ b/pkg/chart/loader/archive/archive.go @@ -32,15 +32,6 @@ import ( "time" ) -// MaxDecompressedChartSize is the maximum size of a chart archive that will be -// decompressed. This is the decompressed size of all the files. -// The default value is 100 MiB. -var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB - -// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load. -// The size of the file is the decompressed version of it when it is stored in an archive. -var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB - var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) var utf8bom = []byte{0xEF, 0xBB, 0xBF} @@ -52,10 +43,33 @@ type BufferedFile struct { Data []byte } +// Options controls archive loading limits. +type Options struct { + // MaxDecompressedChartSize is the maximum size of a chart archive that will be + // decompressed. This is the decompressed size of all the files. + MaxDecompressedChartSize int64 + // MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load. + // The size of the file is the decompressed version of it when it is stored in an archive. + MaxDecompressedFileSize int64 +} + +// DefaultOptions provides the default size limits used when loading archives. +var DefaultOptions = Options{ + MaxDecompressedChartSize: 100 * 1024 * 1024, // 100 MiB + MaxDecompressedFileSize: 5 * 1024 * 1024, // 5 MiB +} + // LoadArchiveFiles reads in files out of an archive into memory. This function // performs important path security checks and should always be used before -// expanding a tarball +// expanding a tarball. It use default options. func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { + return LoadArchiveFilesWithOptions(in, DefaultOptions) +} + +// LoadArchiveFiles reads in files out of an archive into memory. This function +// performs important path security checks and should always be used before +// expanding a tarball. It uses the provided options. +func LoadArchiveFilesWithOptions(in io.Reader, opts Options) ([]*BufferedFile, error) { unzipped, err := gzip.NewReader(in) if err != nil { return nil, err @@ -64,7 +78,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { files := []*BufferedFile{} tr := tar.NewReader(unzipped) - remainingSize := MaxDecompressedChartSize + remainingSize := opts.MaxDecompressedChartSize for { b := bytes.NewBuffer(nil) hd, err := tr.Next() @@ -125,11 +139,11 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { } if hd.Size > remainingSize { - return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d bytes", opts.MaxDecompressedChartSize) } - if hd.Size > MaxDecompressedFileSize { - return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize) + if hd.Size > opts.MaxDecompressedFileSize { + return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d bytes", hd.Name, opts.MaxDecompressedFileSize) } limitedReader := io.LimitReader(tr, remainingSize) @@ -145,7 +159,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { // is the one that goes over the limit. It assumes the Size stored in the tar header // is correct, something many applications do. if bytesWritten < hd.Size || remainingSize <= 0 { - return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d bytes", opts.MaxDecompressedChartSize) } data := bytes.TrimPrefix(b.Bytes(), utf8bom) diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go index dbc5d3004..343fd772a 100644 --- a/pkg/chart/loader/load.go +++ b/pkg/chart/loader/load.go @@ -37,6 +37,7 @@ import ( // ChartLoader loads a chart. type ChartLoader interface { Load() (chart.Charter, error) + LoadWithOptions() (chart.Charter, error) } // Loader returns a new ChartLoader appropriate for the given chart name @@ -46,9 +47,31 @@ func Loader(name string) (ChartLoader, error) { return nil, err } if fi.IsDir() { - return DirLoader(name), nil + return NewDefaultDirLoader(name), nil } - return FileLoader(name), nil + return NewDefaultFileLoader(name), nil +} + +// WithOptions returns a new ChartLoader appropriate for the given chart name +// with the provided options. +func WithOptions(name string, opts archive.Options) (ChartLoader, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return NewDirLoader(name, opts), nil + } + return NewFileLoader(name, opts), nil +} + +// LoadWithOptions takes a string name, resolves it to a file or directory, and loads it with custom options. +func LoadWithOptions(name string, opts archive.Options) (chart.Charter, error) { + l, err := WithOptions(name, opts) + if err != nil { + return nil, err + } + return l.LoadWithOptions() } // Load takes a string name, tries to resolve it to a file or directory, and then loads it. @@ -67,14 +90,36 @@ func Load(name string) (chart.Charter, error) { } // DirLoader loads a chart from a directory -type DirLoader string +type DirLoader struct { + path string + opts archive.Options +} + +// NewDefaultDirLoader creates a new directory loader with default options +func NewDefaultDirLoader(path string) DirLoader { + return DirLoader{path: path, opts: archive.DefaultOptions} +} + +// NewDirLoader creates a new directory loader with custom options +func NewDirLoader(path string, opts archive.Options) DirLoader { + return DirLoader{path: path, opts: opts} +} // Load loads the chart func (l DirLoader) Load() (chart.Charter, error) { - return LoadDir(string(l)) + return LoadDir(l.path) +} + +// LoadWithOptions loads the chart with custom options +func (l DirLoader) LoadWithOptions() (chart.Charter, error) { + return LoadDirWithOptions(l.path, l.opts) } func LoadDir(dir string) (chart.Charter, error) { + return LoadDirWithOptions(dir, archive.DefaultOptions) +} + +func LoadDirWithOptions(dir string, opts archive.Options) (chart.Charter, error) { topdir, err := filepath.Abs(dir) if err != nil { return nil, err @@ -94,24 +139,46 @@ func LoadDir(dir string) (chart.Charter, error) { switch c.APIVersion { case c2.APIVersionV1, c2.APIVersionV2, "": - return c2load.Load(dir) + return c2load.LoadWithOptions(dir, opts) case c3.APIVersionV3: - return c3load.Load(dir) + return c3load.LoadWithOptions(dir, opts) default: return nil, errors.New("unsupported chart version") } } -// FileLoader loads a chart from a file -type FileLoader string +// FileLoader with embedded options +type FileLoader struct { + path string + opts archive.Options +} + +// NewFileLoader creates a file loader with custom options +func NewFileLoader(path string, opts archive.Options) FileLoader { + return FileLoader{path: path, opts: opts} +} + +// NewDefaultFileLoader creates a file loader with default options +func NewDefaultFileLoader(path string) FileLoader { + return FileLoader{path: path, opts: archive.DefaultOptions} +} -// Load loads a chart +// Load loads a chart with default options func (l FileLoader) Load() (chart.Charter, error) { - return LoadFile(string(l)) + return LoadFileWithOptions(l.path, archive.DefaultOptions) +} + +// LoadWithOptions loads a chart with custom options +func (l FileLoader) LoadWithOptions() (chart.Charter, error) { + return LoadFileWithOptions(l.path, l.opts) } func LoadFile(name string) (chart.Charter, error) { + return LoadFileWithOptions(name, archive.DefaultOptions) +} + +func LoadFileWithOptions(name string, opts archive.Options) (chart.Charter, error) { if fi, err := os.Stat(name); err != nil { return nil, err } else if fi.IsDir() { @@ -129,12 +196,12 @@ func LoadFile(name string) (chart.Charter, error) { return nil, err } - files, err := archive.LoadArchiveFiles(raw) + files, err := archive.LoadArchiveFilesWithOptions(raw, opts) if err != nil { if err == gzip.ErrHeader { return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) } - return nil, errors.New("unable to load chart archive") + return nil, fmt.Errorf("unable to load chart archive: %w", err) } for _, f := range files { @@ -145,9 +212,9 @@ func LoadFile(name string) (chart.Charter, error) { } switch c.APIVersion { case c2.APIVersionV1, c2.APIVersionV2, "": - return c2load.Load(name) + return c2load.LoadWithOptions(name, opts) case c3.APIVersionV3: - return c3load.Load(name) + return c3load.LoadWithOptions(name, opts) default: return nil, errors.New("unsupported chart version") } diff --git a/pkg/chart/v2/loader/archive.go b/pkg/chart/v2/loader/archive.go index f6ed0e84f..83f0063e9 100644 --- a/pkg/chart/v2/loader/archive.go +++ b/pkg/chart/v2/loader/archive.go @@ -37,6 +37,11 @@ func (l FileLoader) Load() (*chart.Chart, error) { // LoadFile loads from an archive file. func LoadFile(name string) (*chart.Chart, error) { + return LoadFileWithOptions(name, archive.DefaultOptions) +} + +// LoadFileWithOptions loads from an archive file using the provided options. +func LoadFileWithOptions(name string, opts archive.Options) (*chart.Chart, error) { if fi, err := os.Stat(name); err != nil { return nil, err } else if fi.IsDir() { @@ -54,7 +59,7 @@ func LoadFile(name string) (*chart.Chart, error) { return nil, err } - c, err := LoadArchive(raw) + c, err := LoadArchiveWithOptions(raw, opts) if err != nil { if err == gzip.ErrHeader { return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) @@ -65,10 +70,14 @@ func LoadFile(name string) (*chart.Chart, error) { // LoadArchive loads from a reader containing a compressed tar archive. func LoadArchive(in io.Reader) (*chart.Chart, error) { - files, err := archive.LoadArchiveFiles(in) + return LoadArchiveWithOptions(in, archive.DefaultOptions) +} + +// LoadArchiveWithOptions loads from a reader containing a compressed tar archive using the provided options. +func LoadArchiveWithOptions(in io.Reader, opts archive.Options) (*chart.Chart, error) { + files, err := archive.LoadArchiveFilesWithOptions(in, opts) if err != nil { return nil, err } - return LoadFiles(files) } diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index 82578d924..9a68efd61 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -43,6 +43,11 @@ func (l DirLoader) Load() (*chart.Chart, error) { // // This loads charts only from directories. func LoadDir(dir string) (*chart.Chart, error) { + return LoadDirWithOptions(dir, archive.DefaultOptions) +} + +// LoadDirWithOptions loads from a directory using the provided options. +func LoadDirWithOptions(dir string, opts archive.Options) (*chart.Chart, error) { topdir, err := filepath.Abs(dir) if err != nil { return nil, err @@ -100,8 +105,8 @@ func LoadDir(dir string) (*chart.Chart, error) { return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) } - if fi.Size() > archive.MaxDecompressedFileSize { - return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) + if fi.Size() > opts.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), opts.MaxDecompressedFileSize) } data, err := os.ReadFile(name) diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index ba3a9b6bc..57a69270f 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -61,11 +61,19 @@ func Loader(name string) (ChartLoader, error) { // If a .helmignore file is present, the directory loader will skip loading any files // matching it. But .helmignore is not evaluated when reading out of an archive. func Load(name string) (*chart.Chart, error) { - l, err := Loader(name) + return LoadWithOptions(name, archive.DefaultOptions) +} + +// LoadWithOptions takes a string name, resolves it to a file or directory, and loads it with custom options. +func LoadWithOptions(name string, opts archive.Options) (*chart.Chart, error) { + fi, err := os.Stat(name) if err != nil { return nil, err } - return l.Load() + if fi.IsDir() { + return LoadDirWithOptions(name, opts) + } + return LoadFileWithOptions(name, opts) } // LoadFiles loads from in-memory files. diff --git a/pkg/chart/v2/util/expand.go b/pkg/chart/v2/util/expand.go index 077dfbf38..a6308a0a9 100644 --- a/pkg/chart/v2/util/expand.go +++ b/pkg/chart/v2/util/expand.go @@ -30,9 +30,14 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" ) -// Expand uncompresses and extracts a chart into the specified directory. +// Expand uncompresses and extracts a chart into the specified directory using default options. func Expand(dir string, r io.Reader) error { - files, err := archive.LoadArchiveFiles(r) + return ExpandWithOptions(dir, r, archive.DefaultOptions) +} + +// ExpandWithOptions uncompresses and extracts a chart into the specified directory using the provided options. +func ExpandWithOptions(dir string, r io.Reader, opts archive.Options) error { + files, err := archive.LoadArchiveFilesWithOptions(r, opts) if err != nil { return err } @@ -83,12 +88,17 @@ func Expand(dir string, r io.Reader) error { return nil } -// ExpandFile expands the src file into the dest directory. +// ExpandFile expands the src file into the dest directory using default options. func ExpandFile(dest, src string) error { + return ExpandFileWithOptions(dest, src, archive.DefaultOptions) +} + +// ExpandFileWithOptions expands the src file into the dest directory using the provided options. +func ExpandFileWithOptions(dest, src string, opts archive.Options) error { h, err := os.Open(src) if err != nil { return err } defer h.Close() - return Expand(dest, h) + return ExpandWithOptions(dest, h, opts) } diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 106d24336..31e3c50f5 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -93,6 +93,10 @@ type EnvSettings struct { ColorMode string // ContentCache is the location where cached charts are stored ContentCache string + // MaxChartSize is the maximum size of a decompressed chart in bytes + MaxChartSize int64 + // MaxChartFileSize is the maximum size of a single file in a chart in bytes + MaxChartFileSize int64 } func New() *EnvSettings { @@ -115,6 +119,8 @@ func New() *EnvSettings { BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), QPS: envFloat32Or("HELM_QPS", defaultQPS), ColorMode: envColorMode(), + MaxChartSize: envInt64Or("HELM_MAX_CHART_SIZE", 100*1024*1024), // 100 MiB + MaxChartFileSize: envInt64Or("HELM_MAX_FILE_SIZE", 5*1024*1024), // 5 MiB } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -214,6 +220,20 @@ func envFloat32Or(name string, def float32) float32 { return float32(ret) } +// We want to handle int64 like returned by https://pkg.go.dev/io/fs#FileInfo +func envInt64Or(name string, def int64) int64 { + if name == "" { + return def + } + envVal := envOr(name, strconv.FormatInt(def, 10)) + ret, err := strconv.ParseInt(envVal, 10, 64) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Environment variable %s has invalid value %q (expected an integer): %v\n", name, envVal, err) + return def + } + return ret +} + func envCSV(name string) (ls []string) { trimmed := strings.Trim(os.Getenv(name), ", ") if trimmed != "" { @@ -255,6 +275,8 @@ func (s *EnvSettings) EnvVars() map[string]string { "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), "HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit), "HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32), + "HELM_MAX_CHART_SIZE": strconv.FormatInt(s.MaxChartSize, 10), + "HELM_MAX_FILE_SIZE": strconv.FormatInt(s.MaxChartFileSize, 10), // broken, these are populated from helm flags and not kubeconfig. "HELM_KUBECONTEXT": s.KubeContext, diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 52326eeff..76ee15af6 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -242,6 +242,59 @@ func TestEnvOrBool(t *testing.T) { } } +func TestEnvInt64Or(t *testing.T) { + envName := "TEST_ENV_INT64" + + tests := []struct { + name string + env string + val string + def int64 + expected int64 + }{ + { + name: "empty env with default", + env: "", + val: "", + def: 100, + expected: 100, + }, + { + name: "env set with valid int64", + env: envName, + val: "12345", + def: 100, + expected: 12345, + }, + { + name: "env fails parsing with default", + env: envName, + val: "NOT_A_NUMBER", + def: 100, + expected: 100, + }, + { + name: "env empty string with default", + env: envName, + val: "", + def: 200, + expected: 200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != "" { + t.Setenv(tt.env, tt.val) + } + actual := envInt64Or(tt.env, tt.def) + if actual != tt.expected { + t.Errorf("expected result %d, got %d", tt.expected, actual) + } + }) + } +} + func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) { defer resetEnv()() diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index e5d311394..149b86fdf 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -33,6 +33,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" + "helm.sh/helm/v4/pkg/chart/loader/archive" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -179,6 +180,8 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addDryRunFlag(cmd) bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer, settings) + f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") + f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") return cmd } @@ -256,8 +259,16 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } + opts := archive.DefaultOptions + if client.MaxChartSize > 0 { + opts.MaxDecompressedChartSize = client.MaxChartSize + } + if client.MaxChartFileSize > 0 { + opts.MaxDecompressedFileSize = client.MaxChartFileSize + } + // Check chart dependencies to make sure all are present in /charts - chartRequested, err := loader.Load(cp) + chartRequested, err := loader.LoadWithOptions(cp, opts) if err != nil { return nil, err } diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index f0f12e4f7..2ae791ba7 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -274,6 +274,12 @@ func TestInstall(t *testing.T) { wantError: true, golden: "output/install-hide-secret.txt", }, + { + name: "install with restricted max size", + cmd: "install too-big testdata/testcharts/compressedchart-0.1.0.tgz --max-chart-size=42", + wantError: true, + golden: "output/install-with-restricted-chart-size.txt", + }, } runTestCmd(t, tests) diff --git a/pkg/cmd/pull.go b/pkg/cmd/pull.go index 922698c6c..c24941a2d 100644 --- a/pkg/cmd/pull.go +++ b/pkg/cmd/pull.go @@ -89,6 +89,8 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification") f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this") + f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") + f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") addChartPathOptionsFlags(f, &client.ChartPathOptions) err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/pkg/cmd/pull_test.go b/pkg/cmd/pull_test.go index 96631fe05..024e1aac7 100644 --- a/pkg/cmd/pull_test.go +++ b/pkg/cmd/pull_test.go @@ -210,6 +210,12 @@ func TestPullCmd(t *testing.T) { wantErrorMsg: "chart reference and version mismatch: 0.1.0 is not 0.2.0", wantError: true, }, + { + name: "Fail because of small max chart size", + args: "test/test1 --max-chart-size=90", + wantError: true, + wantErrorMsg: "decompressed chart is larger than the maximum size 90 bytes", + }, } contentCache := t.TempDir() diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index b64118a50..8268fb93b 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -84,6 +84,8 @@ Environment variables: | $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values | | $HELM_COLOR | set color output mode. Allowed values: never, always, auto (default: never) | | $NO_COLOR | set to any non-empty value to disable all colored output (overrides $HELM_COLOR) | +| $HELM_MAX_CHART_SIZE | set the maximum size in bytes for a decompressed chart (default: 100MiB, 0 means use default limit) | +| $HELM_MAX_FILE_SIZE | set the maximum size in bytes for a single file in a chart (default: 5MiB, 0 means use default limit) | Helm stores cache, configuration, and data based on the following configuration order: diff --git a/pkg/cmd/testdata/output/env-comp.txt b/pkg/cmd/testdata/output/env-comp.txt index 9d38ee464..6f560ef58 100644 --- a/pkg/cmd/testdata/output/env-comp.txt +++ b/pkg/cmd/testdata/output/env-comp.txt @@ -13,6 +13,8 @@ HELM_KUBECONTEXT HELM_KUBEINSECURE_SKIP_TLS_VERIFY HELM_KUBETLS_SERVER_NAME HELM_KUBETOKEN +HELM_MAX_CHART_SIZE +HELM_MAX_FILE_SIZE HELM_MAX_HISTORY HELM_NAMESPACE HELM_PLUGINS diff --git a/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt b/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt new file mode 100644 index 000000000..75e30d611 --- /dev/null +++ b/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt @@ -0,0 +1 @@ +Error: INSTALLATION FAILED: unable to load chart archive: decompressed chart is larger than the maximum size 42 bytes diff --git a/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt b/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt new file mode 100644 index 000000000..1ea17076f --- /dev/null +++ b/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt @@ -0,0 +1 @@ +Error: unable to load chart archive: decompressed chart is larger than the maximum size 52 bytes diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 92f130f60..efa366785 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -32,6 +32,7 @@ import ( "helm.sh/helm/v4/pkg/action" ci "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" + "helm.sh/helm/v4/pkg/chart/loader/archive" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -190,8 +191,16 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } + opts := archive.DefaultOptions + if client.MaxChartSize > 0 { + opts.MaxDecompressedChartSize = client.MaxChartSize + } + if client.MaxChartFileSize > 0 { + opts.MaxDecompressedFileSize = client.MaxChartFileSize + } + // Check chart dependencies to make sure all are present in /charts - ch, err := loader.Load(chartPath) + ch, err := loader.LoadWithOptions(chartPath, opts) if err != nil { return err } @@ -219,7 +228,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } // Reload the chart with the updated Chart.lock file. - if ch, err = loader.Load(chartPath); err != nil { + if ch, err = loader.LoadWithOptions(chartPath, opts); err != nil { return fmt.Errorf("failed reloading chart after repo update: %w", err) } } else { @@ -297,6 +306,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources") + f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") + f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") addDryRunFlag(cmd) addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 8729be0ec..656c2bb8c 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -83,6 +83,7 @@ func TestUpgradeCmd(t *testing.T) { missingDepsPath := "testdata/testcharts/chart-missing-deps" badDepsPath := "testdata/testcharts/chart-bad-requirements" presentDepsPath := "testdata/testcharts/chart-with-subchart-update" + compressedPath := "testdata/testcharts/compressedchart-0.1.0.tgz" relWithStatusMock := func(n string, v int, ch *chart.Chart, status rcommon.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) @@ -190,6 +191,12 @@ func TestUpgradeCmd(t *testing.T) { golden: "output/upgrade-uninstalled-with-keep-history.txt", rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusUninstalled)}, }, + { + name: "upgrade with restricted max size", + cmd: fmt.Sprintf("upgrade too-big '%s' --max-chart-size=52", compressedPath), + wantError: true, + golden: "output/upgrade-failed-max-chart-size.txt", + }, } runTestCmd(t, tests) } From 688a49b1c9c65a4615b4324594335327b21d06b2 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 11:37:48 +0100 Subject: [PATCH 2/8] feat: permit using K8s quantity values as HELM_MAX_* Signed-off-by: Benoit Tigeot --- pkg/cli/environment.go | 38 +++++++++++++++----- pkg/cli/environment_test.go | 72 ++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 14 deletions(-) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 31e3c50f5..627a5b6f0 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -25,12 +25,14 @@ package cli import ( "fmt" + "log/slog" "net/http" "os" "strconv" "strings" "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" @@ -119,8 +121,8 @@ func New() *EnvSettings { BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), QPS: envFloat32Or("HELM_QPS", defaultQPS), ColorMode: envColorMode(), - MaxChartSize: envInt64Or("HELM_MAX_CHART_SIZE", 100*1024*1024), // 100 MiB - MaxChartFileSize: envInt64Or("HELM_MAX_FILE_SIZE", 5*1024*1024), // 5 MiB + MaxChartSize: envInt64OrQuantityBytes("HELM_MAX_CHART_SIZE", 100*1024*1024), // 100 MiB + MaxChartFileSize: envInt64OrQuantityBytes("HELM_MAX_FILE_SIZE", 5*1024*1024), // 5 MiB } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -220,18 +222,36 @@ func envFloat32Or(name string, def float32) float32 { return float32(ret) } -// We want to handle int64 like returned by https://pkg.go.dev/io/fs#FileInfo -func envInt64Or(name string, def int64) int64 { +// Tries to parse as a k8s Quantity first, falls back to plain int64 parsing. +func envInt64OrQuantityBytes(name string, def int64) int64 { if name == "" { return def } - envVal := envOr(name, strconv.FormatInt(def, 10)) - ret, err := strconv.ParseInt(envVal, 10, 64) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Environment variable %s has invalid value %q (expected an integer): %v\n", name, envVal, err) + envVal := os.Getenv(name) + if envVal == "" { return def } - return ret + + envVal = strings.TrimSpace(envVal) + + if q, err := resource.ParseQuantity(envVal); err == nil { + if v, ok := q.AsInt64(); ok { + return v + } + f := q.AsApproximateFloat64() + if f > 0 && f < float64(^uint64(0)>>1) { + return int64(f) + } + slog.Warn("Environment variable %s is too large to fit in int64: %q", name, envVal) + return def + } + + if v, err := strconv.ParseInt(envVal, 10, 64); err == nil { + return v + } + + slog.Warn("Environment variable %s has invalid value %q (expected int or k8s Quantity like 512Mi): using default %d", name, envVal, def) + return def } func envCSV(name string) (ls []string) { diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 76ee15af6..3d8b39b2f 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -242,7 +242,7 @@ func TestEnvOrBool(t *testing.T) { } } -func TestEnvInt64Or(t *testing.T) { +func TestEnvInt64OrQuantityBytes(t *testing.T) { envName := "TEST_ENV_INT64" tests := []struct { @@ -253,9 +253,9 @@ func TestEnvInt64Or(t *testing.T) { expected int64 }{ { - name: "empty env with default", + name: "empty env name uses default", env: "", - val: "", + val: "999", def: 100, expected: 100, }, @@ -280,16 +280,78 @@ func TestEnvInt64Or(t *testing.T) { def: 200, expected: 200, }, + + // Quantity cases (bytes) + { + name: "quantity Mi", + env: envName, + val: "512Mi", + def: 100, + expected: 512 * 1024 * 1024, + }, + { + name: "quantity Gi", + env: envName, + val: "2Gi", + def: 100, + expected: 2 * 1024 * 1024 * 1024, + }, + { + name: "quantity Ki", + env: envName, + val: "4096Ki", + def: 100, + expected: 4096 * 1024, + }, + { + name: "decimal SI 1G (base10)", + env: envName, + val: "1G", + def: 100, + // 1G in decimal SI is 1,000,000,000 bytes + expected: 1_000_000_000, + }, + { + name: "decimal SI 500M (base10)", + env: envName, + val: "500M", + def: 100, + expected: 500_000_000, + }, + { + name: "lowercase suffix returns default with error message", + env: envName, + val: "1gi", + def: 100, + expected: 100, // Returns default but prints error about uppercase requirement + }, + { + name: "whitespace trimmed", + env: envName, + val: " 256Mi ", + def: 100, + expected: 256 * 1024 * 1024, + }, + { + name: "too large to fit in int64 returns default", + env: envName, + // ~9.22e18 is max int64; use larger than that to trigger overflow handling. + val: "10000000000Gi", // 10,000,000,000 * 1024^3 bytes ≈ 1.07e22 + def: 1234, + expected: 1234, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Clear previous value to avoid bleed between tests + t.Setenv(envName, "") if tt.env != "" { t.Setenv(tt.env, tt.val) } - actual := envInt64Or(tt.env, tt.def) + actual := envInt64OrQuantityBytes(tt.env, tt.def) if actual != tt.expected { - t.Errorf("expected result %d, got %d", tt.expected, actual) + t.Errorf("expected result %d, got %d (env=%q val=%q def=%d)", tt.expected, actual, tt.env, tt.val, tt.def) } }) } From b42fd71ef40103e17833aaaba60f22a35ea826ec Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 11:46:57 +0100 Subject: [PATCH 3/8] feat: make it possible to pass Kubernetes Quantity to cmd flags too Signed-off-by: Benoit Tigeot --- internal/chart/v3/loader/directory.go | 5 +- pkg/chart/loader/archive/archive.go | 11 ++- pkg/chart/v2/loader/directory.go | 5 +- pkg/cli/environment.go | 75 +++++++++++++++---- pkg/cli/environment_test.go | 62 +++++++++++++++ pkg/cmd/install.go | 7 +- pkg/cmd/pull.go | 8 +- pkg/cmd/pull_test.go | 7 +- .../install-with-restricted-chart-size.txt | 2 +- .../output/upgrade-failed-max-chart-size.txt | 2 +- pkg/cmd/upgrade.go | 8 +- 11 files changed, 161 insertions(+), 31 deletions(-) diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index b49874f76..413e7b34d 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -23,6 +23,8 @@ import ( "path/filepath" "strings" + "k8s.io/apimachinery/pkg/api/resource" + chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/sympath" "helm.sh/helm/v4/pkg/chart/loader/archive" @@ -123,7 +125,8 @@ func LoadDirWithOptions(dir string, opts archive.Options) (*chart.Chart, error) } if fi.Size() > opts.MaxDecompressedFileSize { - return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), opts.MaxDecompressedFileSize) + maxSize := resource.NewQuantity(opts.MaxDecompressedFileSize, resource.BinarySI) + return fmt.Errorf("chart file %q is larger than the maximum file size %s", fi.Name(), maxSize.String()) } data, err := os.ReadFile(name) diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go index 032cc7675..d7682e623 100644 --- a/pkg/chart/loader/archive/archive.go +++ b/pkg/chart/loader/archive/archive.go @@ -30,6 +30,8 @@ import ( "regexp" "strings" "time" + + "k8s.io/apimachinery/pkg/api/resource" ) var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) @@ -139,11 +141,13 @@ func LoadArchiveFilesWithOptions(in io.Reader, opts Options) ([]*BufferedFile, e } if hd.Size > remainingSize { - return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d bytes", opts.MaxDecompressedChartSize) + maxSize := resource.NewQuantity(opts.MaxDecompressedChartSize, resource.BinarySI) + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %s", maxSize.String()) } if hd.Size > opts.MaxDecompressedFileSize { - return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d bytes", hd.Name, opts.MaxDecompressedFileSize) + maxSize := resource.NewQuantity(opts.MaxDecompressedFileSize, resource.BinarySI) + return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %s", hd.Name, maxSize.String()) } limitedReader := io.LimitReader(tr, remainingSize) @@ -159,7 +163,8 @@ func LoadArchiveFilesWithOptions(in io.Reader, opts Options) ([]*BufferedFile, e // is the one that goes over the limit. It assumes the Size stored in the tar header // is correct, something many applications do. if bytesWritten < hd.Size || remainingSize <= 0 { - return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d bytes", opts.MaxDecompressedChartSize) + maxSize := resource.NewQuantity(opts.MaxDecompressedChartSize, resource.BinarySI) + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %s", maxSize.String()) } data := bytes.TrimPrefix(b.Bytes(), utf8bom) diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index 9a68efd61..468ad5a6d 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -23,6 +23,8 @@ import ( "path/filepath" "strings" + "k8s.io/apimachinery/pkg/api/resource" + "helm.sh/helm/v4/internal/sympath" "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -106,7 +108,8 @@ func LoadDirWithOptions(dir string, opts archive.Options) (*chart.Chart, error) } if fi.Size() > opts.MaxDecompressedFileSize { - return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), opts.MaxDecompressedFileSize) + maxSize := resource.NewQuantity(opts.MaxDecompressedFileSize, resource.BinarySI) + return fmt.Errorf("chart file %q is larger than the maximum file size %s", fi.Name(), maxSize.String()) } data, err := os.ReadFile(name) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 627a5b6f0..606780c5f 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -222,6 +222,31 @@ func envFloat32Or(name string, def float32) float32 { return float32(ret) } +// parseQuantityOrInt64 parses a string as either a Kubernetes Quantity or plain int64. +// Returns the parsed value and an error if parsing fails. +func parseQuantityOrInt64(s string) (int64, error) { + s = strings.TrimSpace(s) + + // Try parsing as Kubernetes Quantity first + if q, err := resource.ParseQuantity(s); err == nil { + if v, ok := q.AsInt64(); ok { + return v, nil + } + f := q.AsApproximateFloat64() + if f > 0 && f < float64(^uint64(0)>>1) { + return int64(f), nil + } + return 0, fmt.Errorf("quantity %q is too large to fit in int64", s) + } + + // Fallback to plain int64 + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid value %q (expected int or k8s Quantity like 512Mi)", s) + } + return v, nil +} + // Tries to parse as a k8s Quantity first, falls back to plain int64 parsing. func envInt64OrQuantityBytes(name string, def int64) int64 { if name == "" { @@ -232,26 +257,46 @@ func envInt64OrQuantityBytes(name string, def int64) int64 { return def } - envVal = strings.TrimSpace(envVal) - - if q, err := resource.ParseQuantity(envVal); err == nil { - if v, ok := q.AsInt64(); ok { - return v - } - f := q.AsApproximateFloat64() - if f > 0 && f < float64(^uint64(0)>>1) { - return int64(f) - } - slog.Warn("Environment variable %s is too large to fit in int64: %q", name, envVal) + v, err := parseQuantityOrInt64(envVal) + if err != nil { + defQuantity := resource.NewQuantity(def, resource.BinarySI) + slog.Warn(err.Error() + fmt.Sprintf(": using default %s", defQuantity.String())) return def } + return v +} - if v, err := strconv.ParseInt(envVal, 10, 64); err == nil { - return v +// QuantityBytesValue is a custom flag type that accepts both plain int64 and k8s Quantity formats +type QuantityBytesValue struct { + value *int64 +} + +// NewQuantityBytesValue creates a new QuantityBytesValue flag with a pointer to an int64 +func NewQuantityBytesValue(p *int64) *QuantityBytesValue { + return &QuantityBytesValue{value: p} +} + +// Set parses the input string as either a Kubernetes Quantity or plain int64 +func (q *QuantityBytesValue) Set(s string) error { + v, err := parseQuantityOrInt64(s) + if err != nil { + return err + } + *q.value = v + return nil +} + +// String returns the string representation of the value +func (q *QuantityBytesValue) String() string { + if q.value == nil { + return "0" } + return strconv.FormatInt(*q.value, 10) +} - slog.Warn("Environment variable %s has invalid value %q (expected int or k8s Quantity like 512Mi): using default %d", name, envVal, def) - return def +// Type returns the type name for help messages +func (q *QuantityBytesValue) Type() string { + return "quantity" } func envCSV(name string) (ls []string) { diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 3d8b39b2f..18cb8bc59 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -372,6 +372,68 @@ func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) { } } +func TestQuantityBytesValue(t *testing.T) { + tests := []struct { + name string + input string + expected int64 + expectError bool + }{ + { + name: "plain int64", + input: "12345", + expected: 12345, + }, + { + name: "quantity Mi", + input: "256Mi", + expected: 256 * 1024 * 1024, + }, + { + name: "quantity Gi", + input: "1Gi", + expected: 1 * 1024 * 1024 * 1024, + }, + { + name: "quantity with whitespace", + input: " 512Mi ", + expected: 512 * 1024 * 1024, + }, + { + name: "invalid value", + input: "not-a-number", + expectError: true, + }, + { + name: "lowercase suffix rejected", + input: "1gi", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var val int64 + qv := NewQuantityBytesValue(&val) + + err := qv.Set(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if val != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, val) + } + } + }) + } +} + func resetEnv() func() { origEnv := os.Environ() diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 149b86fdf..0516a7a26 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -34,6 +34,7 @@ import ( "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" "helm.sh/helm/v4/pkg/chart/loader/archive" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -132,6 +133,8 @@ charts in a repository, use 'helm search'. func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewInstall(cfg) + client.MaxChartSize = settings.MaxChartSize + client.MaxChartFileSize = settings.MaxChartFileSize valueOpts := &values.Options{} var outfmt output.Format @@ -180,8 +183,8 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addDryRunFlag(cmd) bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer, settings) - f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") - f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartSize), "max-chart-size", "maximum size for a decompressed chart (e.g., 500Ki, 5Mi)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartFileSize), "max-file-size", "maximum size for a single file in a chart (e.g., 5Mi, 10Mi)") return cmd } diff --git a/pkg/cmd/pull.go b/pkg/cmd/pull.go index c24941a2d..01d7e24cf 100644 --- a/pkg/cmd/pull.go +++ b/pkg/cmd/pull.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cmd/require" ) @@ -45,6 +46,9 @@ result in an error, and the chart will not be saved locally. func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewPull(action.WithConfig(cfg)) + // Initialize from environment settings so they serve as defaults for the flags + client.MaxChartSize = settings.MaxChartSize + client.MaxChartFileSize = settings.MaxChartFileSize cmd := &cobra.Command{ Use: "pull [chart URL | repo/chartname] [...]", @@ -89,8 +93,8 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification") f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this") - f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") - f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartSize), "max-chart-size", "maximum size for a decompressed chart (e.g., 100Mi, 1Gi; default is 100Mi)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartFileSize), "max-file-size", "maximum size for a single file in a chart (e.g., 5Mi, 10Mi; default is 5Mi)") addChartPathOptionsFlags(f, &client.ChartPathOptions) err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/pkg/cmd/pull_test.go b/pkg/cmd/pull_test.go index 024e1aac7..eae095e2b 100644 --- a/pkg/cmd/pull_test.go +++ b/pkg/cmd/pull_test.go @@ -207,14 +207,15 @@ func TestPullCmd(t *testing.T) { { name: "Fail fetching OCI chart with version mismatch", args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.2.0 --version 0.1.0", ociSrv.RegistryURL), - wantErrorMsg: "chart reference and version mismatch: 0.1.0 is not 0.2.0", wantError: true, + wantErrorMsg: "chart reference and version mismatch: 0.1.0 is not 0.2.0", + failExpect: "chart reference and version mismatch", }, { name: "Fail because of small max chart size", - args: "test/test1 --max-chart-size=90", + args: "test/signtest --untar --max-chart-size=1Ki", wantError: true, - wantErrorMsg: "decompressed chart is larger than the maximum size 90 bytes", + wantErrorMsg: "decompressed chart is larger than the maximum size 1Ki", }, } diff --git a/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt b/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt index 75e30d611..565547228 100644 --- a/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt +++ b/pkg/cmd/testdata/output/install-with-restricted-chart-size.txt @@ -1 +1 @@ -Error: INSTALLATION FAILED: unable to load chart archive: decompressed chart is larger than the maximum size 42 bytes +Error: INSTALLATION FAILED: unable to load chart archive: decompressed chart is larger than the maximum size 42 diff --git a/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt b/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt index 1ea17076f..5bf16467a 100644 --- a/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt +++ b/pkg/cmd/testdata/output/upgrade-failed-max-chart-size.txt @@ -1 +1 @@ -Error: unable to load chart archive: decompressed chart is larger than the maximum size 52 bytes +Error: unable to load chart archive: decompressed chart is larger than the maximum size 52 diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index efa366785..ca4757b92 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -33,6 +33,7 @@ import ( ci "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/loader" "helm.sh/helm/v4/pkg/chart/loader/archive" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -84,6 +85,9 @@ which can contain sensitive values. To hide Kubernetes Secrets use the func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewUpgrade(cfg) + // Initialize from environment settings so they serve as defaults for the flags + client.MaxChartSize = settings.MaxChartSize + client.MaxChartFileSize = settings.MaxChartFileSize valueOpts := &values.Options{} var outfmt output.Format var createNamespace bool @@ -306,8 +310,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources") - f.Int64Var(&client.MaxChartSize, "max-chart-size", settings.MaxChartSize, "maximum size in bytes for a decompressed chart (default is 100mb)") - f.Int64Var(&client.MaxChartFileSize, "max-file-size", settings.MaxChartFileSize, "maximum size in bytes for a single file in a chart (default is 5mb)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartSize), "max-chart-size", "maximum size for a decompressed chart (e.g., 100Mi, 1Gi; default is 100Mi)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartFileSize), "max-file-size", "maximum size for a single file in a chart (e.g., 5Mi, 10Mi; default is 5Mi)") addDryRunFlag(cmd) addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) From e7e92cbeb51dcb8a3331a6feabec59aa61b772b9 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 17:36:02 +0100 Subject: [PATCH 4/8] test: add max-chart-size validation test case Signed-off-by: Benoit Tigeot --- pkg/cmd/pull_test.go | 4 +- .../testdata/testcharts/bigchart-0.1.0.tgz | Bin 0 -> 7092 bytes .../testdata/testcharts/bigchart/.helmignore | 23 ++ .../testdata/testcharts/bigchart/Chart.yaml | 24 +++ .../testcharts/bigchart/templates/NOTES.txt | 35 ++++ .../bigchart/templates/_helpers.tpl | 62 ++++++ .../bigchart/templates/deployment.yaml | 78 +++++++ .../testcharts/bigchart/templates/hpa.yaml | 32 +++ .../bigchart/templates/httproute.yaml | 38 ++++ .../bigchart/templates/ingress.yaml | 43 ++++ .../bigchart/templates/service.yaml | 15 ++ .../bigchart/templates/serviceaccount.yaml | 13 ++ .../templates/tests/test-connection.yaml | 15 ++ .../testdata/testcharts/bigchart/values.yaml | 198 ++++++++++++++++++ 14 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 pkg/cmd/testdata/testcharts/bigchart-0.1.0.tgz create mode 100644 pkg/cmd/testdata/testcharts/bigchart/.helmignore create mode 100644 pkg/cmd/testdata/testcharts/bigchart/Chart.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml create mode 100644 pkg/cmd/testdata/testcharts/bigchart/values.yaml diff --git a/pkg/cmd/pull_test.go b/pkg/cmd/pull_test.go index eae095e2b..6d34d9f80 100644 --- a/pkg/cmd/pull_test.go +++ b/pkg/cmd/pull_test.go @@ -213,9 +213,9 @@ func TestPullCmd(t *testing.T) { }, { name: "Fail because of small max chart size", - args: "test/signtest --untar --max-chart-size=1Ki", + args: "test/bigchart --untar --max-chart-size=3Ki", wantError: true, - wantErrorMsg: "decompressed chart is larger than the maximum size 1Ki", + wantErrorMsg: "decompressed chart is larger than the maximum size 3Ki", }, } diff --git a/pkg/cmd/testdata/testcharts/bigchart-0.1.0.tgz b/pkg/cmd/testdata/testcharts/bigchart-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..f00a831e972e971c7f0d9ef3470a06df63318290 GIT binary patch literal 7092 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

cluTiz_O4p3&ZP)n5rn<@i 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 literal 0 HcmV?d00001 diff --git a/pkg/cmd/testdata/testcharts/bigchart/.helmignore b/pkg/cmd/testdata/testcharts/bigchart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml b/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml new file mode 100644 index 000000000..9a46e7810 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: bigchart +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/bigchart/templates/NOTES.txt b/pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt new file mode 100644 index 000000000..534829abd --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt @@ -0,0 +1,35 @@ +1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bigchart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bigchart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bigchart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bigchart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl b/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl new file mode 100644 index 000000000..3f9a279f9 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bigchart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bigchart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bigchart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bigchart.labels" -}} +helm.sh/chart: {{ include "bigchart.chart" . }} +{{ include "bigchart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bigchart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bigchart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bigchart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bigchart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml new file mode 100644 index 000000000..777a486ab --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bigchart.fullname" . }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "bigchart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "bigchart.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "bigchart.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml new file mode 100644 index 000000000..1dbc7c2d0 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bigchart.fullname" . }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bigchart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml new file mode 100644 index 000000000..cc0c5c66f --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "bigchart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml new file mode 100644 index 000000000..d6010215b --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "bigchart.fullname" . }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "bigchart.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml new file mode 100644 index 000000000..ce0599f5f --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bigchart.fullname" . }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "bigchart.selectorLabels" . | nindent 4 }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml new file mode 100644 index 000000000..31c7bb778 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "bigchart.serviceAccountName" . }} + labels: + {{- include "bigchart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml new file mode 100644 index 000000000..9b25bdeff --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "bigchart.fullname" . }}-test-connection" + labels: + {{- include "bigchart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "bigchart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/pkg/cmd/testdata/testcharts/bigchart/values.yaml b/pkg/cmd/testdata/testcharts/bigchart/values.yaml new file mode 100644 index 000000000..3641cd7e0 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/bigchart/values.yaml @@ -0,0 +1,198 @@ +# Default values for bigchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created. + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Large data section to make this chart exceed 1KB when compressed +# This is used for testing the --max-chart-size flag +largeData: | + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure + dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat + non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error + sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi + architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, + sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum + quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam + aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi + ut aliquid ex ea commodi consequatur. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae + consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur. At vero eos et accusamus et iusto odio dignissimos + ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati + cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et + harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil + impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus + autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae + non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur + aut perferendis doloribus asperiores repellat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium + tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna + felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis + risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. + Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, + consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. + Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. + Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Fusce convallis, mauris imperdiet + gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum urna sed risus. In convallis tellus a mauris. Curabitur non elit + ut libero tristique sodales. Mauris a lacus. Donec mattis semper leo. In hac habitasse platea dictumst. Vivamus facilisis diam at + odio. Mauris dictum, nisi eget consequat elementum, lacus ligula molestie metus, non feugiat orci magna ac sem. Donec turpis ipsum, + mattis non, malesuada nec, nonummy vel, lorem. Vivamus facilisis diam at odio. Mauris dictum, nisi eget consequat elementum, lacus + ligula molestie metus, non feugiat orci magna ac sem. From d3cd80d00c291bbba61e63d951f73c8b52ed5b93 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 18:11:05 +0100 Subject: [PATCH 5/8] test: add env var tests for HELM_MAX_CHART_SIZE and HELM_MAX_FILE_SIZE Signed-off-by: Benoit Tigeot --- pkg/cli/environment_test.go | 26 ++-------- pkg/cmd/install_test.go | 51 +++++++++++++++++++ .../install-with-restricted-file-size.txt | 1 + ...upgrade-with-restricted-chart-size-env.txt | 1 + .../upgrade-with-restricted-file-size-env.txt | 1 + pkg/cmd/upgrade_test.go | 51 +++++++++++++++++++ 6 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 pkg/cmd/testdata/output/install-with-restricted-file-size.txt create mode 100644 pkg/cmd/testdata/output/upgrade-with-restricted-chart-size-env.txt create mode 100644 pkg/cmd/testdata/output/upgrade-with-restricted-file-size-env.txt diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 18cb8bc59..8413da83a 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -373,6 +373,8 @@ func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) { } func TestQuantityBytesValue(t *testing.T) { + // This test only verifies that the pflag.Value wrapper correctly propagates + // values and errors. Comprehensive parsing logic is tested in TestEnvInt64OrQuantityBytes. tests := []struct { name string input string @@ -380,35 +382,15 @@ func TestQuantityBytesValue(t *testing.T) { expectError bool }{ { - name: "plain int64", - input: "12345", - expected: 12345, - }, - { - name: "quantity Mi", + name: "valid quantity sets value", input: "256Mi", expected: 256 * 1024 * 1024, }, { - name: "quantity Gi", - input: "1Gi", - expected: 1 * 1024 * 1024 * 1024, - }, - { - name: "quantity with whitespace", - input: " 512Mi ", - expected: 512 * 1024 * 1024, - }, - { - name: "invalid value", + name: "invalid value propagates error", input: "not-a-number", expectError: true, }, - { - name: "lowercase suffix rejected", - input: "1gi", - expectError: true, - }, } for _, tt := range tests { diff --git a/pkg/cmd/install_test.go b/pkg/cmd/install_test.go index 2ae791ba7..0ade6ce86 100644 --- a/pkg/cmd/install_test.go +++ b/pkg/cmd/install_test.go @@ -23,6 +23,7 @@ import ( "path/filepath" "testing" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/repo/v1/repotest" ) @@ -285,6 +286,56 @@ func TestInstall(t *testing.T) { runTestCmd(t, tests) } +func TestInstallWithEnvVars(t *testing.T) { + tests := []struct { + name string + cmd string + envVars map[string]string + wantError bool + golden string + }{ + { + name: "install with HELM_MAX_CHART_SIZE env var with bytes", + cmd: "install too-big testdata/testcharts/compressedchart-0.1.0.tgz", + envVars: map[string]string{ + "HELM_MAX_CHART_SIZE": "42", + }, + wantError: true, + golden: "output/install-with-restricted-chart-size.txt", + }, + { + name: "install with HELM_MAX_FILE_SIZE env var with Quantity suffix", + cmd: "install test-max-file testdata/testcharts/bigchart-0.1.0.tgz", + envVars: map[string]string{ + "HELM_MAX_FILE_SIZE": "1Ki", + }, + wantError: true, + golden: "output/install-with-restricted-file-size.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + for k, v := range tt.envVars { + t.Setenv(k, v) + } + // Reset settings to pick up env vars + settings = cli.New() + + test := cmdTestCase{ + name: tt.name, + cmd: tt.cmd, + golden: tt.golden, + wantError: tt.wantError, + } + + runTestCmd(t, []cmdTestCase{test}) + }) + } +} + func TestInstallOutputCompletion(t *testing.T) { outputFlagCompletionTest(t, "install") } diff --git a/pkg/cmd/testdata/output/install-with-restricted-file-size.txt b/pkg/cmd/testdata/output/install-with-restricted-file-size.txt new file mode 100644 index 000000000..f5792e6a3 --- /dev/null +++ b/pkg/cmd/testdata/output/install-with-restricted-file-size.txt @@ -0,0 +1 @@ +Error: INSTALLATION FAILED: unable to load chart archive: decompressed chart file "bigchart/Chart.yaml" is larger than the maximum file size 1Ki diff --git a/pkg/cmd/testdata/output/upgrade-with-restricted-chart-size-env.txt b/pkg/cmd/testdata/output/upgrade-with-restricted-chart-size-env.txt new file mode 100644 index 000000000..f11530e94 --- /dev/null +++ b/pkg/cmd/testdata/output/upgrade-with-restricted-chart-size-env.txt @@ -0,0 +1 @@ +Error: unable to load chart archive: decompressed chart is larger than the maximum size 10 diff --git a/pkg/cmd/testdata/output/upgrade-with-restricted-file-size-env.txt b/pkg/cmd/testdata/output/upgrade-with-restricted-file-size-env.txt new file mode 100644 index 000000000..f85050f65 --- /dev/null +++ b/pkg/cmd/testdata/output/upgrade-with-restricted-file-size-env.txt @@ -0,0 +1 @@ +Error: unable to load chart archive: decompressed chart file "bigchart/values.yaml" is larger than the maximum file size 2Ki diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 656c2bb8c..b3ce12b55 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -29,6 +29,7 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/cli" rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -201,6 +202,56 @@ func TestUpgradeCmd(t *testing.T) { runTestCmd(t, tests) } +func TestUpgradeWithEnvVars(t *testing.T) { + tests := []struct { + name string + cmd string + envVars map[string]string + wantError bool + golden string + }{ + { + name: "upgrade with HELM_MAX_CHART_SIZE env var with bytes", + cmd: "upgrade too-big testdata/testcharts/compressedchart-0.1.0.tgz", + envVars: map[string]string{ + "HELM_MAX_CHART_SIZE": "10", + }, + wantError: true, + golden: "output/upgrade-with-restricted-chart-size-env.txt", + }, + { + name: "upgrade with HELM_MAX_FILE_SIZE env var with Quantity suffix", + cmd: "upgrade test-max-file testdata/testcharts/bigchart-0.1.0.tgz", + envVars: map[string]string{ + "HELM_MAX_FILE_SIZE": "2Ki", + }, + wantError: true, + golden: "output/upgrade-with-restricted-file-size-env.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + for k, v := range tt.envVars { + t.Setenv(k, v) + } + // Reset settings to pick up env vars + settings = cli.New() + + test := cmdTestCase{ + name: tt.name, + cmd: tt.cmd, + golden: tt.golden, + wantError: tt.wantError, + } + + runTestCmd(t, []cmdTestCase{test}) + }) + } +} + func TestUpgradeWithValue(t *testing.T) { releaseName := "funny-bunny-v2" relMock, ch, chartPath := prepareMockRelease(t, releaseName) From 34799006eef55b0712ac89dbdc95b08438848408 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 6 Nov 2025 18:32:47 +0100 Subject: [PATCH 6/8] test: reduce the complexity of the "bigchart" Signed-off-by: Benoit Tigeot --- .../testdata/testcharts/bigchart/.helmignore | 23 -- .../testdata/testcharts/bigchart/Chart.yaml | 22 +- .../testcharts/bigchart/templates/NOTES.txt | 35 --- .../bigchart/templates/_helpers.tpl | 56 +---- .../bigchart/templates/deployment.yaml | 78 ------- .../testcharts/bigchart/templates/hpa.yaml | 32 --- .../bigchart/templates/httproute.yaml | 38 ---- .../bigchart/templates/ingress.yaml | 43 ---- .../bigchart/templates/service.yaml | 15 -- .../bigchart/templates/serviceaccount.yaml | 13 -- .../templates/tests/test-connection.yaml | 15 -- .../testdata/testcharts/bigchart/values.yaml | 213 +++--------------- 12 files changed, 32 insertions(+), 551 deletions(-) delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/.helmignore delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml delete mode 100644 pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml diff --git a/pkg/cmd/testdata/testcharts/bigchart/.helmignore b/pkg/cmd/testdata/testcharts/bigchart/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml b/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml index 9a46e7810..cffbf2773 100644 --- a/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml +++ b/pkg/cmd/testdata/testcharts/bigchart/Chart.yaml @@ -1,24 +1,6 @@ apiVersion: v2 name: bigchart -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. +description: A test chart for size limit testing 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" +appVersion: "1.0.0" diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt b/pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt deleted file mode 100644 index 534829abd..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/NOTES.txt +++ /dev/null @@ -1,35 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.httpRoute.enabled }} -{{- if .Values.httpRoute.hostnames }} - export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} -{{- else }} - export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") - {{- end }} -{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} - echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" - - NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. - The rules can be set for path, method, header and query parameters. - You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' -{{- end }} -{{- else if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "bigchart.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "bigchart.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "bigchart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "bigchart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl b/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl index 3f9a279f9..d06150caa 100644 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl +++ b/pkg/cmd/testdata/testcharts/bigchart/templates/_helpers.tpl @@ -1,62 +1,14 @@ {{/* -Expand the name of the chart. +Simple helper for testing - returns the chart name */}} {{- define "bigchart.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- .Chart.Name }} {{- end }} {{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. +Simple helper for testing - returns the release name */}} {{- define "bigchart.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "bigchart.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- .Release.Name }} {{- end }} -{{/* -Common labels -*/}} -{{- define "bigchart.labels" -}} -helm.sh/chart: {{ include "bigchart.chart" . }} -{{ include "bigchart.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "bigchart.selectorLabels" -}} -app.kubernetes.io/name: {{ include "bigchart.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "bigchart.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "bigchart.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml deleted file mode 100644 index 777a486ab..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/deployment.yaml +++ /dev/null @@ -1,78 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "bigchart.fullname" . }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "bigchart.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "bigchart.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "bigchart.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ .Chart.Name }} - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 12 }} - {{- end }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - {{- with .Values.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.resources }} - resources: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml deleted file mode 100644 index 1dbc7c2d0..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "bigchart.fullname" . }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "bigchart.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml deleted file mode 100644 index cc0c5c66f..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/httproute.yaml +++ /dev/null @@ -1,38 +0,0 @@ -{{- if .Values.httpRoute.enabled -}} -{{- $fullName := include "bigchart.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ $fullName }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} - {{- with .Values.httpRoute.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - parentRefs: - {{- with .Values.httpRoute.parentRefs }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.httpRoute.hostnames }} - hostnames: - {{- toYaml . | nindent 4 }} - {{- end }} - rules: - {{- range .Values.httpRoute.rules }} - {{- with .matches }} - - matches: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .filters }} - filters: - {{- toYaml . | nindent 8 }} - {{- end }} - backendRefs: - - name: {{ $fullName }} - port: {{ $svcPort }} - weight: 1 - {{- end }} -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml deleted file mode 100644 index d6010215b..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/ingress.yaml +++ /dev/null @@ -1,43 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "bigchart.fullname" . }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- with .Values.ingress.className }} - ingressClassName: {{ . }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- with .pathType }} - pathType: {{ . }} - {{- end }} - backend: - service: - name: {{ include "bigchart.fullname" $ }} - port: - number: {{ $.Values.service.port }} - {{- end }} - {{- end }} -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml deleted file mode 100644 index ce0599f5f..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "bigchart.fullname" . }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "bigchart.selectorLabels" . | nindent 4 }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml deleted file mode 100644 index 31c7bb778..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "bigchart.serviceAccountName" . }} - labels: - {{- include "bigchart.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml b/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml deleted file mode 100644 index 9b25bdeff..000000000 --- a/pkg/cmd/testdata/testcharts/bigchart/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "bigchart.fullname" . }}-test-connection" - labels: - {{- include "bigchart.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "bigchart.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/pkg/cmd/testdata/testcharts/bigchart/values.yaml b/pkg/cmd/testdata/testcharts/bigchart/values.yaml index 3641cd7e0..3349d7ead 100644 --- a/pkg/cmd/testdata/testcharts/bigchart/values.yaml +++ b/pkg/cmd/testdata/testcharts/bigchart/values.yaml @@ -1,198 +1,37 @@ -# Default values for bigchart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. +# Test values for bigchart - used for testing HELM_MAX_CHART_SIZE and HELM_MAX_FILE_SIZE -# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ replicaCount: 1 -# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: repository: nginx - # This sets the pull policy for images. pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" + tag: "1.21.0" -# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ -imagePullSecrets: [] -# This is to override the chart name. -nameOverride: "" -fullnameOverride: "" - -# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ -serviceAccount: - # Specifies whether a service account should be created. - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account. - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template. - name: "" - -# This is for setting Kubernetes Annotations to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ -podAnnotations: {} -# This is for setting Kubernetes Labels to a Pod. -# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ service: - # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types type: ClusterIP - # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports port: 80 -# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -# -- Expose the service via gateway-api HTTPRoute -# Requires Gateway API resources and suitable controller installed within the cluster -# (see: https://gateway-api.sigs.k8s.io/guides/) -httpRoute: - # HTTPRoute enabled. - enabled: false - # HTTPRoute annotations. - annotations: {} - # Which Gateways this Route is attached to. - parentRefs: - - name: gateway - sectionName: http - # namespace: default - # Hostnames matching HTTP header. - hostnames: - - chart-example.local - # List of rules and filters applied. - rules: - - matches: - - path: - type: PathPrefix - value: /headers - # filters: - # - type: RequestHeaderModifier - # requestHeaderModifier: - # set: - # - name: My-Overwrite-Header - # value: this-is-the-only-value - # remove: - # - User-Agent - # - matches: - # - path: - # type: PathPrefix - # value: /echo - # headers: - # - name: version - # value: v2 - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ -livenessProbe: - httpGet: - path: / - port: http -readinessProbe: - httpGet: - path: / - port: http - -# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] - # - name: foo - # secret: - # secretName: mysecret - # optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] - # - name: foo - # mountPath: "/etc/foo" - # readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} - -# Large data section to make this chart exceed 1KB when compressed -# This is used for testing the --max-chart-size flag -largeData: | - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat - non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error - sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi - architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, - sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum - quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam - aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi - ut aliquid ex ea commodi consequatur. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae - consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur. At vero eos et accusamus et iusto odio dignissimos - ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati - cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et - harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil - impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus - autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae - non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur - aut perferendis doloribus asperiores repellat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip - ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Curabitur pretium - tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna - felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis - risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. - Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, - consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. - Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. - Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. Fusce convallis, mauris imperdiet - gravida bibendum, nisl turpis suscipit mauris, sed placerat ipsum urna sed risus. In convallis tellus a mauris. Curabitur non elit - ut libero tristique sodales. Mauris a lacus. Donec mattis semper leo. In hac habitasse platea dictumst. Vivamus facilisis diam at - odio. Mauris dictum, nisi eget consequat elementum, lacus ligula molestie metus, non feugiat orci magna ac sem. Donec turpis ipsum, - mattis non, malesuada nec, nonummy vel, lorem. Vivamus facilisis diam at odio. Mauris dictum, nisi eget consequat elementum, lacus - ligula molestie metus, non feugiat orci magna ac sem. +# Large data section to make this chart exceed 3KB when compressed +# This is used for testing size limit environment variables +testData: + # Each item below is approximately 132 characters + item001: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item002: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item003: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item004: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item005: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item006: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item007: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item008: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item009: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item010: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item011: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item012: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item013: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item014: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item015: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item016: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item017: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item018: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item019: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + item020: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." \ No newline at end of file From 892bdf95a98dc8220f38ad2732cf40d7f04a4b40 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 7 Nov 2025 11:29:20 +0100 Subject: [PATCH 7/8] fix: reject invalid 'm' suffix in byte size parsing Rename parseQuantityOrInt64 to parseByteSizeOrInt64 and improve error handling for invalid byte size quantities. Now properly rejects 'm' (milli) suffix with a helpful message suggesting IEC values (Ki, Mi, Gi). Also fixes misleading "too large" error for sub-byte quantities. Signed-off-by: Benoit Tigeot --- pkg/cli/environment.go | 25 +++++++++++++++++-------- pkg/cli/environment_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 606780c5f..2d30770a1 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -222,9 +222,9 @@ func envFloat32Or(name string, def float32) float32 { return float32(ret) } -// parseQuantityOrInt64 parses a string as either a Kubernetes Quantity or plain int64. -// Returns the parsed value and an error if parsing fails. -func parseQuantityOrInt64(s string) (int64, error) { +// parseByteSizeOrInt64 parses a string as either a Kubernetes Quantity or plain int64, +// specifically for byte sizes. Returns the parsed value in bytes +func parseByteSizeOrInt64(s string) (int64, error) { s = strings.TrimSpace(s) // Try parsing as Kubernetes Quantity first @@ -233,10 +233,19 @@ func parseQuantityOrInt64(s string) (int64, error) { return v, nil } f := q.AsApproximateFloat64() - if f > 0 && f < float64(^uint64(0)>>1) { - return int64(f), nil + // Reject quantities that evaluate to less than 1 byte (e.g. "1m" -> 0.001) + // because file sizes must be whole bytes. Treat those as parsing errors. + if f < 1 { + // Provide a helpful message if the user tried to use "m" (milli) suffix + if strings.HasSuffix(strings.ToLower(s), "m") && !strings.HasSuffix(s, "M") { + return 0, fmt.Errorf("quantity %q uses 'm' (milli) suffix which represents 0.001; please use IEC values like Ki, Mi, Gi", s) + } + return 0, fmt.Errorf("quantity %q is too small (less than 1 byte)", s) } - return 0, fmt.Errorf("quantity %q is too large to fit in int64", s) + if f >= float64(^uint64(0)>>1) { + return 0, fmt.Errorf("quantity %q is too large to fit in int64", s) + } + return int64(f), nil } // Fallback to plain int64 @@ -257,7 +266,7 @@ func envInt64OrQuantityBytes(name string, def int64) int64 { return def } - v, err := parseQuantityOrInt64(envVal) + v, err := parseByteSizeOrInt64(envVal) if err != nil { defQuantity := resource.NewQuantity(def, resource.BinarySI) slog.Warn(err.Error() + fmt.Sprintf(": using default %s", defQuantity.String())) @@ -278,7 +287,7 @@ func NewQuantityBytesValue(p *int64) *QuantityBytesValue { // Set parses the input string as either a Kubernetes Quantity or plain int64 func (q *QuantityBytesValue) Set(s string) error { - v, err := parseQuantityOrInt64(s) + v, err := parseByteSizeOrInt64(s) if err != nil { return err } diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index 8413da83a..02604333a 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -325,6 +325,27 @@ func TestEnvInt64OrQuantityBytes(t *testing.T) { def: 100, expected: 100, // Returns default but prints error about uppercase requirement }, + { + name: "suffix Mb rejected", + env: envName, + val: "1000Mb", + def: 100, + expected: 100, // Returns default but prints error about 'Mb' being invalid + }, + { + name: "suffix mo rejected", + env: envName, + val: "1000mo", + def: 100, + expected: 100, // Returns default but prints error about 'mo' being invalid + }, + { + name: "suffix m rejected (milli)", + env: envName, + val: "10m", + def: 100, + expected: 100, // Returns default but prints error about 'm' being invalid + }, { name: "whitespace trimmed", env: envName, @@ -391,6 +412,16 @@ func TestQuantityBytesValue(t *testing.T) { input: "not-a-number", expectError: true, }, + { + name: "Mb suffix rejected", + input: "1Mb", + expectError: true, + }, + { + name: "m suffix rejected", + input: "1m", + expectError: true, + }, } for _, tt := range tests { From 413fa2ea68ce254e3e8cc34ae72299a3f219d80f Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Mon, 10 Nov 2025 19:17:02 +0100 Subject: [PATCH 8/8] Do not forget to use MaxChartFileSize and MaxChartFileSize in template Signed-off-by: Benoit Tigeot --- pkg/cmd/template.go | 3 +++ pkg/cmd/template_test.go | 12 ++++++++++++ .../output/template-with-restricted-chart-size.txt | 1 + .../output/template-with-restricted-file-size.txt | 1 + 4 files changed, 17 insertions(+) create mode 100644 pkg/cmd/testdata/output/template-with-restricted-chart-size.txt create mode 100644 pkg/cmd/testdata/output/template-with-restricted-file-size.txt diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index cf68c6c46..e66d8cf04 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -35,6 +35,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" @@ -194,6 +195,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f := cmd.Flags() addInstallFlags(cmd, f, client, valueOpts) + f.Var(cli.NewQuantityBytesValue(&client.MaxChartSize), "max-chart-size", "maximum size for a decompressed chart (e.g., 500Ki, 5Mi)") + f.Var(cli.NewQuantityBytesValue(&client.MaxChartFileSize), "max-file-size", "maximum size for a single file in a chart (e.g., 5Mi, 10Mi)") f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") f.BoolVar(&validate, "validate", false, "deprecated") diff --git a/pkg/cmd/template_test.go b/pkg/cmd/template_test.go index 5bcccf5d0..1c00b74ac 100644 --- a/pkg/cmd/template_test.go +++ b/pkg/cmd/template_test.go @@ -166,6 +166,18 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), golden: "output/template-subchart-cm-set-file.txt", }, + { + name: "template with restricted max size (with IEC size)", + cmd: "template too-big testdata/testcharts/oci-dependent-chart-0.1.0.tgz --max-chart-size=1Ki", + wantError: true, + golden: "output/template-with-restricted-chart-size.txt", + }, + { + name: "template with restricted max file size", + cmd: "template too-big testdata/testcharts/compressedchart-0.1.0.tgz --max-file-size=10", + wantError: true, + golden: "output/template-with-restricted-file-size.txt", + }, } runTestCmd(t, tests) } diff --git a/pkg/cmd/testdata/output/template-with-restricted-chart-size.txt b/pkg/cmd/testdata/output/template-with-restricted-chart-size.txt new file mode 100644 index 000000000..94acd948d --- /dev/null +++ b/pkg/cmd/testdata/output/template-with-restricted-chart-size.txt @@ -0,0 +1 @@ +Error: unable to load chart archive: decompressed chart is larger than the maximum size 1Ki diff --git a/pkg/cmd/testdata/output/template-with-restricted-file-size.txt b/pkg/cmd/testdata/output/template-with-restricted-file-size.txt new file mode 100644 index 000000000..d08344f7f --- /dev/null +++ b/pkg/cmd/testdata/output/template-with-restricted-file-size.txt @@ -0,0 +1 @@ +Error: unable to load chart archive: decompressed chart file "compressedchart/Chart.yaml" is larger than the maximum file size 10