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