Benoit Tigeot 5 days ago committed by GitHub
commit e421f49523
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 errors.Is(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
}

@ -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"
@ -32,17 +34,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}
}
// 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
// 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 +124,9 @@ 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 {
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)

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

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

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

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

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

@ -30,16 +30,9 @@ import (
"regexp"
"strings"
"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
"k8s.io/apimachinery/pkg/api/resource"
)
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
@ -52,10 +45,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 +80,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 +141,13 @@ 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)
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 > 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 {
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)
@ -145,7 +163,8 @@ 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)
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)

@ -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 errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", 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")
}

@ -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 errors.Is(err, gzip.ErrHeader) {
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", 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)
}

@ -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"
@ -43,6 +45,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 +107,9 @@ 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 {
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)

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

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

@ -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"
@ -93,6 +95,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 +121,8 @@ func New() *EnvSettings {
BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit),
QPS: envFloat32Or("HELM_QPS", defaultQPS),
ColorMode: envColorMode(),
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"))
@ -214,6 +222,92 @@ func envFloat32Or(name string, def float32) float32 {
return float32(ret)
}
// 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
if q, err := resource.ParseQuantity(s); err == nil {
if v, ok := q.AsInt64(); ok {
return v, nil
}
f := q.AsApproximateFloat64()
// 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)
}
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
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 == "" {
return def
}
envVal := os.Getenv(name)
if envVal == "" {
return def
}
v, err := parseByteSizeOrInt64(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
}
// 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 := parseByteSizeOrInt64(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)
}
// Type returns the type name for help messages
func (q *QuantityBytesValue) Type() string {
return "quantity"
}
func envCSV(name string) (ls []string) {
trimmed := strings.Trim(os.Getenv(name), ", ")
if trimmed != "" {
@ -255,6 +349,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,

@ -242,6 +242,142 @@ func TestEnvOrBool(t *testing.T) {
}
}
func TestEnvInt64OrQuantityBytes(t *testing.T) {
envName := "TEST_ENV_INT64"
tests := []struct {
name string
env string
val string
def int64
expected int64
}{
{
name: "empty env name uses default",
env: "",
val: "999",
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,
},
// 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: "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,
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 := envInt64OrQuantityBytes(tt.env, tt.def)
if actual != tt.expected {
t.Errorf("expected result %d, got %d (env=%q val=%q def=%d)", tt.expected, actual, tt.env, tt.val, tt.def)
}
})
}
}
func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) {
defer resetEnv()()
@ -257,6 +393,60 @@ 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
expected int64
expectError bool
}{
{
name: "valid quantity sets value",
input: "256Mi",
expected: 256 * 1024 * 1024,
},
{
name: "invalid value propagates error",
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 {
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()

@ -33,6 +33,8 @@ 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"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
@ -131,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
@ -179,6 +183,8 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addDryRunFlag(cmd)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
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
}
@ -256,8 +262,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
}

@ -23,6 +23,7 @@ import (
"path/filepath"
"testing"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/repo/v1/repotest"
)
@ -274,11 +275,67 @@ 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)
}
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")
}

@ -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,6 +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.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) {

@ -207,8 +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),
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/bigchart --untar --max-chart-size=3Ki",
wantError: true,
wantErrorMsg: "decompressed chart is larger than the maximum size 3Ki",
},
}

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

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

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

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

@ -0,0 +1 @@
Error: INSTALLATION FAILED: unable to load chart archive: decompressed chart is larger than the maximum size 42

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

@ -0,0 +1 @@
Error: unable to load chart archive: decompressed chart is larger than the maximum size 1Ki

@ -0,0 +1 @@
Error: unable to load chart archive: decompressed chart file "compressedchart/Chart.yaml" is larger than the maximum file size 10

@ -0,0 +1 @@
Error: unable to load chart archive: decompressed chart is larger than the maximum size 52

@ -0,0 +1 @@
Error: unable to load chart archive: decompressed chart is larger than the maximum size 10

@ -0,0 +1 @@
Error: unable to load chart archive: decompressed chart file "bigchart/values.yaml" is larger than the maximum file size 2Ki

Binary file not shown.

@ -0,0 +1,6 @@
apiVersion: v2
name: bigchart
description: A test chart for size limit testing
type: application
version: 0.1.0
appVersion: "1.0.0"

@ -0,0 +1,14 @@
{{/*
Simple helper for testing - returns the chart name
*/}}
{{- define "bigchart.name" -}}
{{- .Chart.Name }}
{{- end }}
{{/*
Simple helper for testing - returns the release name
*/}}
{{- define "bigchart.fullname" -}}
{{- .Release.Name }}
{{- end }}

@ -0,0 +1,37 @@
# Test values for bigchart - used for testing HELM_MAX_CHART_SIZE and HELM_MAX_FILE_SIZE
replicaCount: 1
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.21.0"
service:
type: ClusterIP
port: 80
# 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."

@ -32,6 +32,8 @@ 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"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
@ -83,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
@ -192,8 +197,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
}
@ -221,7 +234,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 {
@ -299,6 +312,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.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)

@ -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"
)
@ -83,6 +84,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,10 +192,66 @@ 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)
}
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)

Loading…
Cancel
Save