From 8dc7c57f504603fa6e8c3359f48cf76f40663821 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 17 Sep 2025 15:19:02 -0400 Subject: [PATCH] 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 --- internal/chart/v3/loader/archive.go | 166 +-------------- internal/chart/v3/loader/directory.go | 9 +- internal/chart/v3/loader/load.go | 15 +- internal/chart/v3/loader/load_test.go | 7 +- internal/chart/v3/util/expand.go | 4 +- pkg/action/install.go | 36 +++- pkg/action/install_test.go | 3 +- pkg/action/package.go | 35 +++- pkg/action/upgrade.go | 23 ++- pkg/chart/common.go | 24 +++ pkg/chart/dependency.go | 56 +++++ pkg/chart/interfaces.go | 8 + pkg/chart/loader/archive/archive.go | 195 ++++++++++++++++++ .../chart/loader/archive}/archive_test.go | 2 +- pkg/chart/loader/load.go | 163 +++++++++++++++ pkg/chart/v2/loader/archive.go | 166 +-------------- pkg/chart/v2/loader/archive_test.go | 92 --------- pkg/chart/v2/loader/directory.go | 9 +- pkg/chart/v2/loader/load.go | 15 +- pkg/chart/v2/loader/load_test.go | 11 +- pkg/chart/v2/util/expand.go | 4 +- pkg/cmd/install.go | 23 ++- pkg/cmd/upgrade.go | 12 +- 23 files changed, 590 insertions(+), 488 deletions(-) create mode 100644 pkg/chart/dependency.go create mode 100644 pkg/chart/loader/archive/archive.go rename {internal/chart/v3/loader => pkg/chart/loader/archive}/archive_test.go (99%) create mode 100644 pkg/chart/loader/load.go delete mode 100644 pkg/chart/v2/loader/archive_test.go diff --git a/internal/chart/v3/loader/archive.go b/internal/chart/v3/loader/archive.go index 311959d56..358c2ce4d 100644 --- a/internal/chart/v3/loader/archive.go +++ b/internal/chart/v3/loader/archive.go @@ -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 } diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 947051604..8cb7323dc 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -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 { diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 2959fc71d..b1b4bba8f 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -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 { diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 1d8ca836a..9f41429cc 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -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:"), diff --git a/internal/chart/v3/util/expand.go b/internal/chart/v3/util/expand.go index 6cbbeabf2..1a10fce3c 100644 --- a/internal/chart/v3/util/expand.go +++ b/internal/chart/v3/util/expand.go @@ -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 } diff --git a/pkg/action/install.go b/pkg/action/install.go index 5ae12904d..c6d4f723c 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -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 { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index b2b1508be..e90857a3f 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -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) diff --git a/pkg/action/package.go b/pkg/action/package.go index 6e762b507..92a9a8cb6 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -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) } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3688adf0e..3c84570b2 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -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") diff --git a/pkg/chart/common.go b/pkg/chart/common.go index 8b1dd58c3..8080f3dc8 100644 --- a/pkg/chart/common.go +++ b/pkg/chart/common.go @@ -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) diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go new file mode 100644 index 000000000..9f7c90364 --- /dev/null +++ b/pkg/chart/dependency.go @@ -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 +} diff --git a/pkg/chart/interfaces.go b/pkg/chart/interfaces.go index e87dd2c08..f9c61c35c 100644 --- a/pkg/chart/interfaces.go +++ b/pkg/chart/interfaces.go @@ -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 } diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go new file mode 100644 index 000000000..4d4ca4391 --- /dev/null +++ b/pkg/chart/loader/archive/archive.go @@ -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) +} diff --git a/internal/chart/v3/loader/archive_test.go b/pkg/chart/loader/archive/archive_test.go similarity index 99% rename from internal/chart/v3/loader/archive_test.go rename to pkg/chart/loader/archive/archive_test.go index d16c47563..2fe09e9b2 100644 --- a/internal/chart/v3/loader/archive_test.go +++ b/pkg/chart/loader/archive/archive_test.go @@ -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" diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go new file mode 100644 index 000000000..7a5ddbca9 --- /dev/null +++ b/pkg/chart/loader/load.go @@ -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"` +} diff --git a/pkg/chart/v2/loader/archive.go b/pkg/chart/v2/loader/archive.go index b9f370f56..f6ed0e84f 100644 --- a/pkg/chart/v2/loader/archive.go +++ b/pkg/chart/v2/loader/archive.go @@ -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 } diff --git a/pkg/chart/v2/loader/archive_test.go b/pkg/chart/v2/loader/archive_test.go deleted file mode 100644 index d16c47563..000000000 --- a/pkg/chart/v2/loader/archive_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index 4f72925dc..c6f31560c 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -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 { diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 0c025e183..028d59e82 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -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 { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index c4ae646f6..7eca5f402 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -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:"), diff --git a/pkg/chart/v2/util/expand.go b/pkg/chart/v2/util/expand.go index 9d08571ed..077dfbf38 100644 --- a/pkg/chart/v2/util/expand.go +++ b/pkg/chart/v2/util/expand.go @@ -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 } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index c4e121c1f..4f30bd7df 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -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 diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index c8fbf8bd3..fcc4f9294 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -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") }