Update the action interfaces for chart apiversions

This change is about handling the interfaces to public functions for
different chart apiVersions. The internals are still focused on v2.
This enables v3 to be layered in layer.

Signed-off-by: Matt Farina <matt.farina@suse.com>
pull/31303/head
Matt Farina 2 days ago
parent 1a06fe9901
commit 8dc7c57f50
No known key found for this signature in database
GPG Key ID: 92C44A3D421FF7F9

@ -17,32 +17,16 @@ limitations under the License.
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// 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]:/`)
// FileLoader loads a chart from a file
type FileLoader string
@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) {
}
defer raw.Close()
err = ensureArchive(name, raw)
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) {
return c, err
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func ensureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}
// 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
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
if hd.Size > MaxDecompressedFileSize {
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
}
limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// 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)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := LoadArchiveFiles(in)
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}

@ -25,6 +25,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
"helm.sh/helm/v4/pkg/ignore"
)
@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
}
rules.AddDefaults()
files := []*BufferedFile{}
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
@ -99,8 +100,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() > MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), MaxDecompressedFileSize)
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -32,6 +32,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// ChartLoader loads a chart.
@ -66,16 +67,10 @@ func Load(name string) (*chart.Chart, error) {
return l.Load()
}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*BufferedFile)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
@ -125,7 +120,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
}
@ -155,7 +150,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*BufferedFile, 0, len(files))
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {

@ -32,6 +32,7 @@ import (
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
func TestLoadDir(t *testing.T) {
@ -183,7 +184,7 @@ func TestLoadFile(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v3
@ -248,7 +249,7 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*BufferedFile{}); err == nil {
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
@ -259,7 +260,7 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),

@ -27,12 +27,12 @@ import (
"sigs.k8s.io/yaml"
chart "helm.sh/helm/v4/internal/chart/v3"
"helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart/loader/archive"
)
// Expand uncompresses and extracts a chart into the specified directory.
func Expand(dir string, r io.Reader) error {
files, err := loader.LoadArchiveFiles(r)
files, err := archive.LoadArchiveFiles(r)
if err != nil {
return err
}

@ -42,6 +42,7 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
@ -243,7 +244,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals)
}
@ -252,7 +253,17 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
//
// When the task is cancelled through ctx, the function returns and the install
// proceeds in the background.
func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (*release.Release, error) {
var chrt *chart.Chart
switch c := ch.(type) {
case *chart.Chart:
chrt = c
case chart.Chart:
chrt = &c
default:
return nil, errors.New("invalid chart apiVersion")
}
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
@ -761,17 +772,30 @@ func TemplateName(nameTemplate string) (string, error) {
}
// CheckDependencies checks the dependencies for a chart.
func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
func CheckDependencies(ch ci.Charter, reqs []ci.Dependency) error {
ac, err := ci.NewAccessor(ch)
if err != nil {
return err
}
var missing []string
OUTER:
for _, r := range reqs {
for _, d := range ch.Dependencies() {
if d.Name() == r.Name {
rac, err := ci.NewDependencyAccessor(r)
if err != nil {
return err
}
for _, d := range ac.Dependencies() {
dac, err := ci.NewAccessor(d)
if err != nil {
return err
}
if dac.Name() == rac.Name() {
continue OUTER
}
}
missing = append(missing, r.Name)
missing = append(missing, rac.Name())
}
if len(missing) > 0 {

@ -1004,7 +1004,8 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) {
instAction := NewInstall(config)
instAction.ClientOnly = false
ctx, done := context.WithCancel(t.Context())
res, err := instAction.RunWithContext(ctx, nil, nil)
chrt := buildChart()
res, err := instAction.RunWithContext(ctx, chrt, nil)
done()
assert.Nil(t, res)

@ -28,7 +28,9 @@ import (
"golang.org/x/term"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/v2/loader"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/provenance"
)
@ -69,7 +71,21 @@ func NewPackage() *Package {
// Run executes 'helm package' against the given chart and returns the path to the packaged chart.
func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch, err := loader.LoadDir(path)
chrt, err := loader.LoadDir(path)
if err != nil {
return "", err
}
var ch *chart.Chart
switch c := chrt.(type) {
case *chart.Chart:
ch = c
case chart.Chart:
ch = &c
default:
return "", errors.New("invalid chart apiVersion")
}
ac, err := ci.NewAccessor(ch)
if err != nil {
return "", err
}
@ -87,7 +103,7 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch.Metadata.AppVersion = p.AppVersion
}
if reqs := ch.Metadata.Dependencies; reqs != nil {
if reqs := ac.MetaDependencies(); reqs != nil {
if err := CheckDependencies(ch, reqs); err != nil {
return "", err
}
@ -146,13 +162,22 @@ func (p *Package) Clearsign(filename string) error {
}
// Load the chart archive to extract metadata
chart, err := loader.LoadFile(filename)
chrt, err := loader.LoadFile(filename)
if err != nil {
return fmt.Errorf("failed to load chart for signing: %w", err)
}
var ch *chart.Chart
switch c := chrt.(type) {
case *chart.Chart:
ch = c
case chart.Chart:
ch = &c
default:
return errors.New("invalid chart apiVersion")
}
// Marshal chart metadata to YAML bytes
metadataBytes, err := yaml.Marshal(chart.Metadata)
metadataBytes, err := yaml.Marshal(ch.Metadata)
if err != nil {
return fmt.Errorf("failed to marshal chart metadata: %w", err)
}

@ -28,9 +28,10 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/postrenderer"
@ -151,17 +152,27 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) {
}
// Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background()
return u.RunWithContext(ctx, name, chart, vals)
}
// RunWithContext executes the upgrade on the given release with context.
func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, vals map[string]interface{}) (*release.Release, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
var chrt *chartv2.Chart
switch c := ch.(type) {
case *chartv2.Chart:
chrt = c
case chartv2.Chart:
chrt = &c
default:
return nil, errors.New("invalid chart apiVersion")
}
// Make sure wait is set if RollbackOnFailure. This makes it so
// the user doesn't have to specify both
if u.WaitStrategy == kube.HookOnlyStrategy && u.RollbackOnFailure {
@ -173,7 +184,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
}
slog.Debug("preparing upgrade", "name", name)
currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chart, vals)
currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals)
if err != nil {
return nil, err
}
@ -206,7 +217,7 @@ func (u *Upgrade) isDryRun() bool {
}
// prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
if chart == nil {
return nil, nil, false, errMissingChart
}
@ -578,7 +589,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
//
// This is skipped if the u.ResetValues flag is set, in which case the
// request values are not altered.
func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
if u.ResetValues {
// If ResetValues is set, we completely ignore current.Config.
slog.Debug("resetting values to the chart's original version")

@ -93,6 +93,14 @@ func (r *v2Accessor) Dependencies() []Charter {
return deps
}
func (r *v2Accessor) MetaDependencies() []Dependency {
var deps = make([]Dependency, len(r.chrt.Metadata.Dependencies))
for i, c := range r.chrt.Metadata.Dependencies {
deps[i] = c
}
return deps
}
func (r *v2Accessor) Values() map[string]interface{} {
return r.chrt.Values
}
@ -101,6 +109,10 @@ func (r *v2Accessor) Schema() []byte {
return r.chrt.Schema
}
func (r *v2Accessor) Deprecated() bool {
return r.chrt.Metadata.Deprecated
}
type v3Accessor struct {
chrt *v3chart.Chart
}
@ -150,6 +162,14 @@ func (r *v3Accessor) Dependencies() []Charter {
return deps
}
func (r *v3Accessor) MetaDependencies() []Dependency {
var deps = make([]Dependency, len(r.chrt.Dependencies()))
for i, c := range r.chrt.Metadata.Dependencies {
deps[i] = c
}
return deps
}
func (r *v3Accessor) Values() map[string]interface{} {
return r.chrt.Values
}
@ -158,6 +178,10 @@ func (r *v3Accessor) Schema() []byte {
return r.chrt.Schema
}
func (r *v3Accessor) Deprecated() bool {
return r.chrt.Metadata.Deprecated
}
func structToMap(obj interface{}) (map[string]interface{}, error) {
objValue := reflect.ValueOf(obj)

@ -0,0 +1,56 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
import (
"errors"
v3chart "helm.sh/helm/v4/internal/chart/v3"
v2chart "helm.sh/helm/v4/pkg/chart/v2"
)
var NewDependencyAccessor func(dep Dependency) (DependencyAccessor, error) = NewDefaultDependencyAccessor //nolint:revive
func NewDefaultDependencyAccessor(dep Dependency) (DependencyAccessor, error) {
switch v := dep.(type) {
case v2chart.Dependency:
return &v2DependencyAccessor{&v}, nil
case *v2chart.Dependency:
return &v2DependencyAccessor{v}, nil
case v3chart.Dependency:
return &v3DependencyAccessor{&v}, nil
case *v3chart.Dependency:
return &v3DependencyAccessor{v}, nil
default:
return nil, errors.New("unsupported chart dependency type")
}
}
type v2DependencyAccessor struct {
dep *v2chart.Dependency
}
func (r *v2DependencyAccessor) Name() string {
return r.dep.Name
}
type v3DependencyAccessor struct {
dep *v3chart.Dependency
}
func (r *v3DependencyAccessor) Name() string {
return r.dep.Name
}

@ -21,6 +21,8 @@ import (
type Charter interface{}
type Dependency interface{}
type Accessor interface {
Name() string
IsRoot() bool
@ -30,6 +32,12 @@ type Accessor interface {
ChartFullPath() string
IsLibraryChart() bool
Dependencies() []Charter
MetaDependencies() []Dependency
Values() map[string]interface{}
Schema() []byte
Deprecated() bool
}
type DependencyAccessor interface {
Name() string
}

@ -0,0 +1,195 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// archive provides utility functions for working with Helm chart archive files
package archive
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
)
// 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}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// 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
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
if hd.Size > MaxDecompressedFileSize {
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
}
limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// 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)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func EnsureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
package archive
import (
"archive/tar"

@ -0,0 +1,163 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"compress/gzip"
"errors"
"fmt"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
c3 "helm.sh/helm/v4/internal/chart/v3"
c3load "helm.sh/helm/v4/internal/chart/v3/loader"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader/archive"
c2 "helm.sh/helm/v4/pkg/chart/v2"
c2load "helm.sh/helm/v4/pkg/chart/v2/loader"
)
// ChartLoader loads a chart.
type ChartLoader interface {
Load() (chart.Charter, error)
}
// Loader returns a new ChartLoader appropriate for the given chart name
func Loader(name string) (ChartLoader, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if fi.IsDir() {
return DirLoader(name), nil
}
return FileLoader(name), nil
}
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
//
// This is the preferred way to load a chart. It will discover the chart encoding
// and hand off to the appropriate chart reader.
//
// 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.Charter, error) {
l, err := Loader(name)
if err != nil {
return nil, err
}
return l.Load()
}
// DirLoader loads a chart from a directory
type DirLoader string
// Load loads the chart
func (l DirLoader) Load() (chart.Charter, error) {
return LoadDir(string(l))
}
func LoadDir(dir string) (chart.Charter, error) {
topdir, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
name := filepath.Join(topdir, "Chart.yaml")
data, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("unable to detect chart at %s: %w", name, err)
}
c := new(chartBase)
err = yaml.Unmarshal(data, c)
if err != nil {
return nil, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
switch c.APIVersion {
case c2.APIVersionV1, c2.APIVersionV2, "":
return c2load.Load(dir)
case c3.APIVersionV3:
return c3load.Load(dir)
default:
return nil, errors.New("unsupported chart version")
}
}
// FileLoader loads a chart from a file
type FileLoader string
// Load loads a chart
func (l FileLoader) Load() (chart.Charter, error) {
return LoadFile(string(l))
}
func LoadFile(name string) (chart.Charter, error) {
if fi, err := os.Stat(name); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("cannot load a directory")
}
raw, err := os.Open(name)
if err != nil {
return nil, err
}
defer raw.Close()
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
files, err := archive.LoadArchiveFiles(raw)
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")
}
for _, f := range files {
if f.Name == "Chart.yaml" {
c := new(chartBase)
if err := yaml.Unmarshal(f.Data, c); err != nil {
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
}
switch c.APIVersion {
case c2.APIVersionV1, c2.APIVersionV2, "":
return c2load.Load(name)
case c3.APIVersionV3:
return c3load.Load(name)
default:
return nil, errors.New("unsupported chart version")
}
}
}
return nil, errors.New("unable to detect chart version, no Chart.yaml found")
}
// chartBase is used to detect the API Version for the chart to run it through the
// loader for that type.
type chartBase struct {
APIVersion string `json:"apiVersion,omitempty"`
}

@ -17,32 +17,16 @@ limitations under the License.
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"regexp"
"strings"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
// 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]:/`)
// FileLoader loads a chart from a file
type FileLoader string
@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) {
}
defer raw.Close()
err = ensureArchive(name, raw)
err = archive.EnsureArchive(name, raw)
if err != nil {
return nil, err
}
@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) {
return c, err
}
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
//
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
// if we didn't check for this.
func ensureArchive(name string, raw *os.File) error {
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
// Check the file format to give us a chance to provide the user with more actionable feedback.
buffer := make([]byte, 512)
_, err := raw.Read(buffer)
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
// Wrong content type. Let's check if it's yaml and give an extra hint?
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
}
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
}
return nil
}
// isGZipApplication checks whether the archive is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}
// 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
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
unzipped, err := gzip.NewReader(in)
if err != nil {
return nil, err
}
defer unzipped.Close()
files := []*BufferedFile{}
tr := tar.NewReader(unzipped)
remainingSize := MaxDecompressedChartSize
for {
b := bytes.NewBuffer(nil)
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hd.FileInfo().IsDir() {
// Use this instead of hd.Typeflag because we don't have to do any
// inference chasing.
continue
}
switch hd.Typeflag {
// We don't want to process these extension header files.
case tar.TypeXGlobalHeader, tar.TypeXHeader:
continue
}
// Archive could contain \ if generated on Windows
delimiter := "/"
if strings.ContainsRune(hd.Name, '\\') {
delimiter = "\\"
}
parts := strings.Split(hd.Name, delimiter)
n := strings.Join(parts[1:], delimiter)
// Normalize the path to the / delimiter
n = strings.ReplaceAll(n, delimiter, "/")
if path.IsAbs(n) {
return nil, errors.New("chart illegally contains absolute paths")
}
n = path.Clean(n)
if n == "." {
// In this case, the original path was relative when it should have been absolute.
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
}
if strings.HasPrefix(n, "..") {
return nil, errors.New("chart illegally references parent directory")
}
// In some particularly arcane acts of path creativity, it is possible to intermix
// UNIX and Windows style paths in such a way that you produce a result of the form
// c:/foo even after all the built-in absolute path checks. So we explicitly check
// for this condition.
if drivePathPattern.MatchString(n) {
return nil, errors.New("chart contains illegally named files")
}
if parts[0] == "Chart.yaml" {
return nil, errors.New("chart yaml not in base directory")
}
if hd.Size > remainingSize {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
}
if hd.Size > MaxDecompressedFileSize {
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
}
limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader)
if err != nil {
return nil, err
}
remainingSize -= bytesWritten
// When the bytesWritten are less than the file size it means the limit reader ended
// copying early. Here we report that error. This is important if the last file extracted
// 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)
}
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
b.Reset()
}
if len(files) == 0 {
return nil, errors.New("no files in chart archive")
}
return files, nil
}
// LoadArchive loads from a reader containing a compressed tar archive.
func LoadArchive(in io.Reader) (*chart.Chart, error) {
files, err := LoadArchiveFiles(in)
files, err := archive.LoadArchiveFiles(in)
if err != nil {
return nil, err
}

@ -1,92 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package loader
import (
"archive/tar"
"bytes"
"compress/gzip"
"testing"
)
func TestLoadArchiveFiles(t *testing.T) {
tcs := []struct {
name string
generate func(w *tar.Writer)
check func(t *testing.T, files []*BufferedFile, err error)
}{
{
name: "empty input should return no files",
generate: func(_ *tar.Writer) {},
check: func(t *testing.T, _ []*BufferedFile, err error) {
t.Helper()
if err.Error() != "no files in chart archive" {
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
}
},
},
{
name: "should ignore files with XGlobalHeader type",
generate: func(w *tar.Writer) {
// simulate the presence of a `pax_global_header` file like you would get when
// processing a GitHub release archive.
err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeXGlobalHeader,
Name: "pax_global_header",
})
if err != nil {
t.Fatal(err)
}
// we need to have at least one file, otherwise we'll get the "no files in chart archive" error
err = w.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: "dir/empty",
})
if err != nil {
t.Fatal(err)
}
},
check: func(t *testing.T, files []*BufferedFile, err error) {
t.Helper()
if err != nil {
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
}
if len(files) != 1 {
t.Fatalf(`expected to get one file but got [%v]`, files)
}
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
buf := &bytes.Buffer{}
gzw := gzip.NewWriter(buf)
tw := tar.NewWriter(gzw)
tc.generate(tw)
_ = tw.Close()
_ = gzw.Close()
files, err := LoadArchiveFiles(buf)
tc.check(t, files, err)
})
}
}

@ -24,6 +24,7 @@ import (
"strings"
"helm.sh/helm/v4/internal/sympath"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/ignore"
)
@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
}
rules.AddDefaults()
files := []*BufferedFile{}
files := []*archive.BufferedFile{}
topdir += string(filepath.Separator)
walk := func(name string, fi os.FileInfo, err error) error {
@ -99,8 +100,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() > MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), MaxDecompressedFileSize)
if fi.Size() > archive.MaxDecompressedFileSize {
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
data = bytes.TrimPrefix(data, utf8bom)
files = append(files, &BufferedFile{Name: n, Data: data})
files = append(files, &archive.BufferedFile{Name: n, Data: data})
return nil
}
if err = sympath.Walk(topdir, walk); err != nil {

@ -32,6 +32,7 @@ import (
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
@ -67,16 +68,10 @@ func Load(name string) (*chart.Chart, error) {
return l.Load()
}
// BufferedFile represents an archive file buffered for later processing.
type BufferedFile struct {
Name string
Data []byte
}
// LoadFiles loads from in-memory files.
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c := new(chart.Chart)
subcharts := make(map[string][]*BufferedFile)
subcharts := make(map[string][]*archive.BufferedFile)
// do not rely on assumed ordering of files in the chart and crash
// if Chart.yaml was not coming early enough to initialize metadata
@ -157,7 +152,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
fname := strings.TrimPrefix(f.Name, "charts/")
cname := strings.SplitN(fname, "/", 2)[0]
subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data})
default:
c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data})
}
@ -187,7 +182,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
default:
// We have to trim the prefix off of every file, and ignore any file
// that is in charts/, but isn't actually a chart.
buff := make([]*BufferedFile, 0, len(files))
buff := make([]*archive.BufferedFile, 0, len(files))
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 {

@ -31,6 +31,7 @@ import (
"time"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
)
@ -211,12 +212,12 @@ func TestLoadFile(t *testing.T) {
func TestLoadFiles_BadCases(t *testing.T) {
for _, tt := range []struct {
name string
bufferedFiles []*BufferedFile
bufferedFiles []*archive.BufferedFile
expectError string
}{
{
name: "These files contain only requirements.lock",
bufferedFiles: []*BufferedFile{
bufferedFiles: []*archive.BufferedFile{
{
Name: "requirements.lock",
Data: []byte(""),
@ -235,7 +236,7 @@ func TestLoadFiles_BadCases(t *testing.T) {
}
func TestLoadFiles(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "Chart.yaml",
Data: []byte(`apiVersion: v1
@ -300,7 +301,7 @@ icon: https://example.com/64x64.png
t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
}
if _, err = LoadFiles([]*BufferedFile{}); err == nil {
if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil {
t.Fatal("Expected err to be non-nil")
}
if err.Error() != "Chart.yaml file is missing" {
@ -311,7 +312,7 @@ icon: https://example.com/64x64.png
// Test the order of file loading. The Chart.yaml file needs to come first for
// later comparison checks. See https://github.com/helm/helm/pull/8948
func TestLoadFilesOrder(t *testing.T) {
goodFiles := []*BufferedFile{
goodFiles := []*archive.BufferedFile{
{
Name: "requirements.yaml",
Data: []byte("dependencies:"),

@ -26,13 +26,13 @@ import (
securejoin "github.com/cyphar/filepath-securejoin"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/chart/loader/archive"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
)
// Expand uncompresses and extracts a chart into the specified directory.
func Expand(dir string, r io.Reader) error {
files, err := loader.LoadArchiveFiles(r)
files, err := archive.LoadArchiveFiles(r)
if err != nil {
return err
}

@ -33,8 +33,8 @@ import (
"github.com/spf13/pflag"
"helm.sh/helm/v4/pkg/action"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
@ -270,15 +270,20 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
return nil, err
}
if err := checkIfInstallable(chartRequested); err != nil {
ac, err := chart.NewAccessor(chartRequested)
if err != nil {
return nil, err
}
if chartRequested.Metadata.Deprecated {
if err := checkIfInstallable(ac); err != nil {
return nil, err
}
if ac.Deprecated() {
slog.Warn("this chart is deprecated")
}
if req := chartRequested.Metadata.Dependencies; req != nil {
if req := ac.MetaDependencies(); req != nil {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
@ -337,12 +342,14 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
// checkIfInstallable validates if a chart can be installed
//
// Application chart type is only installable
func checkIfInstallable(ch *chart.Chart) error {
switch ch.Metadata.Type {
func checkIfInstallable(ch chart.Accessor) error {
meta := ch.MetadataAsMap()
switch meta["Type"] {
case "", "application":
return nil
}
return fmt.Errorf("%s charts are not installable", ch.Metadata.Type)
return fmt.Errorf("%s charts are not installable", meta["Type"])
}
// Provide dynamic auto-completion for the install and template commands

@ -30,7 +30,8 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/v2/loader"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/loader"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
@ -198,7 +199,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
return err
}
if req := ch.Metadata.Dependencies; req != nil {
ac, err := ci.NewAccessor(ch)
if err != nil {
return err
}
if req := ac.MetaDependencies(); req != nil {
if err := action.CheckDependencies(ch, req); err != nil {
err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err)
if client.DependencyUpdate {
@ -226,7 +232,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
}
if ch.Metadata.Deprecated {
if ac.Deprecated() {
slog.Warn("this chart is deprecated")
}

Loading…
Cancel
Save