diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go
new file mode 100644
index 000000000..4d59fa5ec
--- /dev/null
+++ b/internal/chart/v3/chart.go
@@ -0,0 +1,172 @@
+/*
+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 v3
+
+import (
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// APIVersionV3 is the API version number for version 3.
+const APIVersionV3 = "v3"
+
+// aliasNameFormat defines the characters that are legal in an alias name.
+var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
+
+// Chart is a helm package that contains metadata, a default config, zero or more
+// optionally parameterizable templates, and zero or more charts (dependencies).
+type Chart struct {
+ // Raw contains the raw contents of the files originally contained in the chart archive.
+ //
+ // This should not be used except in special cases like `helm show values`,
+ // where we want to display the raw values, comments and all.
+ Raw []*File `json:"-"`
+ // Metadata is the contents of the Chartfile.
+ Metadata *Metadata `json:"metadata"`
+ // Lock is the contents of Chart.lock.
+ Lock *Lock `json:"lock"`
+ // Templates for this chart.
+ Templates []*File `json:"templates"`
+ // Values are default config for this chart.
+ Values map[string]interface{} `json:"values"`
+ // Schema is an optional JSON schema for imposing structure on Values
+ Schema []byte `json:"schema"`
+ // Files are miscellaneous files in a chart archive,
+ // e.g. README, LICENSE, etc.
+ Files []*File `json:"files"`
+
+ parent *Chart
+ dependencies []*Chart
+}
+
+type CRD struct {
+ // Name is the File.Name for the crd file
+ Name string
+ // Filename is the File obj Name including (sub-)chart.ChartFullPath
+ Filename string
+ // File is the File obj for the crd
+ File *File
+}
+
+// SetDependencies replaces the chart dependencies.
+func (ch *Chart) SetDependencies(charts ...*Chart) {
+ ch.dependencies = nil
+ ch.AddDependency(charts...)
+}
+
+// Name returns the name of the chart.
+func (ch *Chart) Name() string {
+ if ch.Metadata == nil {
+ return ""
+ }
+ return ch.Metadata.Name
+}
+
+// AddDependency determines if the chart is a subchart.
+func (ch *Chart) AddDependency(charts ...*Chart) {
+ for i, x := range charts {
+ charts[i].parent = ch
+ ch.dependencies = append(ch.dependencies, x)
+ }
+}
+
+// Root finds the root chart.
+func (ch *Chart) Root() *Chart {
+ if ch.IsRoot() {
+ return ch
+ }
+ return ch.Parent().Root()
+}
+
+// Dependencies are the charts that this chart depends on.
+func (ch *Chart) Dependencies() []*Chart { return ch.dependencies }
+
+// IsRoot determines if the chart is the root chart.
+func (ch *Chart) IsRoot() bool { return ch.parent == nil }
+
+// Parent returns a subchart's parent chart.
+func (ch *Chart) Parent() *Chart { return ch.parent }
+
+// ChartPath returns the full path to this chart in dot notation.
+func (ch *Chart) ChartPath() string {
+ if !ch.IsRoot() {
+ return ch.Parent().ChartPath() + "." + ch.Name()
+ }
+ return ch.Name()
+}
+
+// ChartFullPath returns the full path to this chart.
+// Note that the path may not correspond to the path where the file can be found on the file system if the path
+// points to an aliased subchart.
+func (ch *Chart) ChartFullPath() string {
+ if !ch.IsRoot() {
+ return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()
+ }
+ return ch.Name()
+}
+
+// Validate validates the metadata.
+func (ch *Chart) Validate() error {
+ return ch.Metadata.Validate()
+}
+
+// AppVersion returns the appversion of the chart.
+func (ch *Chart) AppVersion() string {
+ if ch.Metadata == nil {
+ return ""
+ }
+ return ch.Metadata.AppVersion
+}
+
+// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
+// Deprecated: use CRDObjects()
+func (ch *Chart) CRDs() []*File {
+ files := []*File{}
+ // Find all resources in the crds/ directory
+ for _, f := range ch.Files {
+ if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
+ files = append(files, f)
+ }
+ }
+ // Get CRDs from dependencies, too.
+ for _, dep := range ch.Dependencies() {
+ files = append(files, dep.CRDs()...)
+ }
+ return files
+}
+
+// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts
+func (ch *Chart) CRDObjects() []CRD {
+ crds := []CRD{}
+ // Find all resources in the crds/ directory
+ for _, f := range ch.Files {
+ if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
+ mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f}
+ crds = append(crds, mycrd)
+ }
+ }
+ // Get CRDs from dependencies, too.
+ for _, dep := range ch.Dependencies() {
+ crds = append(crds, dep.CRDObjects()...)
+ }
+ return crds
+}
+
+func hasManifestExtension(fname string) bool {
+ ext := filepath.Ext(fname)
+ return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")
+}
diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go
new file mode 100644
index 000000000..f93b3356b
--- /dev/null
+++ b/internal/chart/v3/chart_test.go
@@ -0,0 +1,211 @@
+/*
+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 v3
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCRDs(t *testing.T) {
+ chrt := Chart{
+ Files: []*File{
+ {
+ Name: "crds/foo.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "bar.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crds/foo/bar/baz.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crdsfoo/bar/baz.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crds/README.md",
+ Data: []byte("# hello"),
+ },
+ },
+ }
+
+ is := assert.New(t)
+ crds := chrt.CRDs()
+ is.Equal(2, len(crds))
+ is.Equal("crds/foo.yaml", crds[0].Name)
+ is.Equal("crds/foo/bar/baz.yaml", crds[1].Name)
+}
+
+func TestSaveChartNoRawData(t *testing.T) {
+ chrt := Chart{
+ Raw: []*File{
+ {
+ Name: "fhqwhgads.yaml",
+ Data: []byte("Everybody to the Limit"),
+ },
+ },
+ }
+
+ is := assert.New(t)
+ data, err := json.Marshal(chrt)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ res := &Chart{}
+ if err := json.Unmarshal(data, res); err != nil {
+ t.Fatal(err)
+ }
+
+ is.Equal([]*File(nil), res.Raw)
+}
+
+func TestMetadata(t *testing.T) {
+ chrt := Chart{
+ Metadata: &Metadata{
+ Name: "foo.yaml",
+ AppVersion: "1.0.0",
+ APIVersion: "v3",
+ Version: "1.0.0",
+ Type: "application",
+ },
+ }
+
+ is := assert.New(t)
+
+ is.Equal("foo.yaml", chrt.Name())
+ is.Equal("1.0.0", chrt.AppVersion())
+ is.Equal(nil, chrt.Validate())
+}
+
+func TestIsRoot(t *testing.T) {
+ chrt1 := Chart{
+ parent: &Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ },
+ }
+
+ chrt2 := Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ }
+
+ is := assert.New(t)
+
+ is.Equal(false, chrt1.IsRoot())
+ is.Equal(true, chrt2.IsRoot())
+}
+
+func TestChartPath(t *testing.T) {
+ chrt1 := Chart{
+ parent: &Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ },
+ }
+
+ chrt2 := Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ }
+
+ is := assert.New(t)
+
+ is.Equal("foo.", chrt1.ChartPath())
+ is.Equal("foo", chrt2.ChartPath())
+}
+
+func TestChartFullPath(t *testing.T) {
+ chrt1 := Chart{
+ parent: &Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ },
+ }
+
+ chrt2 := Chart{
+ Metadata: &Metadata{
+ Name: "foo",
+ },
+ }
+
+ is := assert.New(t)
+
+ is.Equal("foo/charts/", chrt1.ChartFullPath())
+ is.Equal("foo", chrt2.ChartFullPath())
+}
+
+func TestCRDObjects(t *testing.T) {
+ chrt := Chart{
+ Files: []*File{
+ {
+ Name: "crds/foo.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "bar.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crds/foo/bar/baz.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crdsfoo/bar/baz.yaml",
+ Data: []byte("hello"),
+ },
+ {
+ Name: "crds/README.md",
+ Data: []byte("# hello"),
+ },
+ },
+ }
+
+ expected := []CRD{
+ {
+ Name: "crds/foo.yaml",
+ Filename: "crds/foo.yaml",
+ File: &File{
+ Name: "crds/foo.yaml",
+ Data: []byte("hello"),
+ },
+ },
+ {
+ Name: "crds/foo/bar/baz.yaml",
+ Filename: "crds/foo/bar/baz.yaml",
+ File: &File{
+ Name: "crds/foo/bar/baz.yaml",
+ Data: []byte("hello"),
+ },
+ },
+ }
+
+ is := assert.New(t)
+ crds := chrt.CRDObjects()
+ is.Equal(expected, crds)
+}
diff --git a/internal/chart/v3/dependency.go b/internal/chart/v3/dependency.go
new file mode 100644
index 000000000..2d956b548
--- /dev/null
+++ b/internal/chart/v3/dependency.go
@@ -0,0 +1,82 @@
+/*
+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 v3
+
+import "time"
+
+// Dependency describes a chart upon which another chart depends.
+//
+// Dependencies can be used to express developer intent, or to capture the state
+// of a chart.
+type Dependency struct {
+ // Name is the name of the dependency.
+ //
+ // This must mach the name in the dependency's Chart.yaml.
+ Name string `json:"name" yaml:"name"`
+ // Version is the version (range) of this chart.
+ //
+ // A lock file will always produce a single version, while a dependency
+ // may contain a semantic version range.
+ Version string `json:"version,omitempty" yaml:"version,omitempty"`
+ // The URL to the repository.
+ //
+ // Appending `index.yaml` to this string should result in a URL that can be
+ // used to fetch the repository index.
+ Repository string `json:"repository" yaml:"repository"`
+ // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
+ Condition string `json:"condition,omitempty" yaml:"condition,omitempty"`
+ // Tags can be used to group charts for enabling/disabling together
+ Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
+ // Enabled bool determines if chart should be loaded
+ Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
+ // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
+ // string or pair of child/parent sublist items.
+ ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"`
+ // Alias usable alias to be used for the chart
+ Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
+}
+
+// Validate checks for common problems with the dependency datastructure in
+// the chart. This check must be done at load time before the dependency's charts are
+// loaded.
+func (d *Dependency) Validate() error {
+ if d == nil {
+ return ValidationError("dependencies must not contain empty or null nodes")
+ }
+ d.Name = sanitizeString(d.Name)
+ d.Version = sanitizeString(d.Version)
+ d.Repository = sanitizeString(d.Repository)
+ d.Condition = sanitizeString(d.Condition)
+ for i := range d.Tags {
+ d.Tags[i] = sanitizeString(d.Tags[i])
+ }
+ if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
+ return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
+ }
+ return nil
+}
+
+// Lock is a lock file for dependencies.
+//
+// It represents the state that the dependencies should be in.
+type Lock struct {
+ // Generated is the date the lock file was last generated.
+ Generated time.Time `json:"generated"`
+ // Digest is a hash of the dependencies in Chart.yaml.
+ Digest string `json:"digest"`
+ // Dependencies is the list of dependencies that this lock file has locked.
+ Dependencies []*Dependency `json:"dependencies"`
+}
diff --git a/internal/chart/v3/dependency_test.go b/internal/chart/v3/dependency_test.go
new file mode 100644
index 000000000..fcea19aea
--- /dev/null
+++ b/internal/chart/v3/dependency_test.go
@@ -0,0 +1,44 @@
+/*
+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 v3
+
+import (
+ "testing"
+)
+
+func TestValidateDependency(t *testing.T) {
+ dep := &Dependency{
+ Name: "example",
+ }
+ for value, shouldFail := range map[string]bool{
+ "abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
+ "-okay": false,
+ "_okay": false,
+ "- bad": true,
+ " bad": true,
+ "bad\nvalue": true,
+ "bad ": true,
+ "bad$": true,
+ } {
+ dep.Alias = value
+ res := dep.Validate()
+ if res != nil && !shouldFail {
+ t.Errorf("Failed on case %q", dep.Alias)
+ } else if res == nil && shouldFail {
+ t.Errorf("Expected failure for %q", dep.Alias)
+ }
+ }
+}
diff --git a/internal/chart/v3/doc.go b/internal/chart/v3/doc.go
new file mode 100644
index 000000000..e003833a0
--- /dev/null
+++ b/internal/chart/v3/doc.go
@@ -0,0 +1,21 @@
+/*
+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 v3 provides chart handling for apiVersion v3 charts
+
+This package and its sub-packages provide handling for apiVersion v3 charts.
+*/
+package v3
diff --git a/internal/chart/v3/errors.go b/internal/chart/v3/errors.go
new file mode 100644
index 000000000..059e43f07
--- /dev/null
+++ b/internal/chart/v3/errors.go
@@ -0,0 +1,30 @@
+/*
+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 v3
+
+import "fmt"
+
+// ValidationError represents a data validation error.
+type ValidationError string
+
+func (v ValidationError) Error() string {
+ return "validation: " + string(v)
+}
+
+// ValidationErrorf takes a message and formatting options and creates a ValidationError
+func ValidationErrorf(msg string, args ...interface{}) ValidationError {
+ return ValidationError(fmt.Sprintf(msg, args...))
+}
diff --git a/internal/chart/v3/file.go b/internal/chart/v3/file.go
new file mode 100644
index 000000000..ba04e106d
--- /dev/null
+++ b/internal/chart/v3/file.go
@@ -0,0 +1,27 @@
+/*
+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 v3
+
+// File represents a file as a name/value pair.
+//
+// By convention, name is a relative path within the scope of the chart's
+// base directory.
+type File struct {
+ // Name is the path-like name of the template.
+ Name string `json:"name"`
+ // Data is the template as byte data.
+ Data []byte `json:"data"`
+}
diff --git a/internal/chart/v3/fuzz_test.go b/internal/chart/v3/fuzz_test.go
new file mode 100644
index 000000000..982c26489
--- /dev/null
+++ b/internal/chart/v3/fuzz_test.go
@@ -0,0 +1,48 @@
+/*
+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 v3
+
+import (
+ "testing"
+
+ fuzz "github.com/AdaLogics/go-fuzz-headers"
+)
+
+func FuzzMetadataValidate(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ fdp := fuzz.NewConsumer(data)
+ // Add random values to the metadata
+ md := &Metadata{}
+ err := fdp.GenerateStruct(md)
+ if err != nil {
+ t.Skip()
+ }
+ md.Validate()
+ })
+}
+
+func FuzzDependencyValidate(f *testing.F) {
+ f.Fuzz(func(t *testing.T, data []byte) {
+ f := fuzz.NewConsumer(data)
+ // Add random values to the dependenci
+ d := &Dependency{}
+ err := f.GenerateStruct(d)
+ if err != nil {
+ t.Skip()
+ }
+ d.Validate()
+ })
+}
diff --git a/internal/chart/v3/loader/archive.go b/internal/chart/v3/loader/archive.go
new file mode 100644
index 000000000..311959d56
--- /dev/null
+++ b/internal/chart/v3/loader/archive.go
@@ -0,0 +1,234 @@
+/*
+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"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// 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
+
+// Load loads a chart
+func (l FileLoader) Load() (*chart.Chart, error) {
+ return LoadFile(string(l))
+}
+
+// LoadFile loads from an archive file.
+func LoadFile(name string) (*chart.Chart, 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 = ensureArchive(name, raw)
+ if err != nil {
+ return nil, err
+ }
+
+ c, err := LoadArchive(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 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)
+ if err != nil {
+ return nil, err
+ }
+
+ return LoadFiles(files)
+}
diff --git a/internal/chart/v3/loader/archive_test.go b/internal/chart/v3/loader/archive_test.go
new file mode 100644
index 000000000..d16c47563
--- /dev/null
+++ b/internal/chart/v3/loader/archive_test.go
@@ -0,0 +1,92 @@
+/*
+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/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go
new file mode 100644
index 000000000..947051604
--- /dev/null
+++ b/internal/chart/v3/loader/directory.go
@@ -0,0 +1,121 @@
+/*
+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 (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/sympath"
+ "helm.sh/helm/v4/pkg/ignore"
+)
+
+var utf8bom = []byte{0xEF, 0xBB, 0xBF}
+
+// DirLoader loads a chart from a directory
+type DirLoader string
+
+// Load loads the chart
+func (l DirLoader) Load() (*chart.Chart, error) {
+ return LoadDir(string(l))
+}
+
+// LoadDir loads from a directory.
+//
+// This loads charts only from directories.
+func LoadDir(dir string) (*chart.Chart, error) {
+ topdir, err := filepath.Abs(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ // Just used for errors.
+ c := &chart.Chart{}
+
+ rules := ignore.Empty()
+ ifile := filepath.Join(topdir, ignore.HelmIgnore)
+ if _, err := os.Stat(ifile); err == nil {
+ r, err := ignore.ParseFile(ifile)
+ if err != nil {
+ return c, err
+ }
+ rules = r
+ }
+ rules.AddDefaults()
+
+ files := []*BufferedFile{}
+ topdir += string(filepath.Separator)
+
+ walk := func(name string, fi os.FileInfo, err error) error {
+ n := strings.TrimPrefix(name, topdir)
+ if n == "" {
+ // No need to process top level. Avoid bug with helmignore .* matching
+ // empty names. See issue 1779.
+ return nil
+ }
+
+ // Normalize to / since it will also work on Windows
+ n = filepath.ToSlash(n)
+
+ if err != nil {
+ return err
+ }
+ if fi.IsDir() {
+ // Directory-based ignore rules should involve skipping the entire
+ // contents of that directory.
+ if rules.Ignore(n, fi) {
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // If a .helmignore file matches, skip this file.
+ if rules.Ignore(n, fi) {
+ return nil
+ }
+
+ // Irregular files include devices, sockets, and other uses of files that
+ // are not regular files. In Go they have a file mode type bit set.
+ // See https://golang.org/pkg/os/#FileMode for examples.
+ if !fi.Mode().IsRegular() {
+ 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)
+ }
+
+ data, err := os.ReadFile(name)
+ if err != nil {
+ return fmt.Errorf("error reading %s: %w", n, err)
+ }
+
+ data = bytes.TrimPrefix(data, utf8bom)
+
+ files = append(files, &BufferedFile{Name: n, Data: data})
+ return nil
+ }
+ if err = sympath.Walk(topdir, walk); err != nil {
+ return c, err
+ }
+
+ return LoadFiles(files)
+}
diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go
new file mode 100644
index 000000000..30bafdad4
--- /dev/null
+++ b/internal/chart/v3/loader/load.go
@@ -0,0 +1,219 @@
+/*
+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 (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "maps"
+ "os"
+ "path/filepath"
+ "strings"
+
+ utilyaml "k8s.io/apimachinery/pkg/util/yaml"
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// ChartLoader loads a chart.
+type ChartLoader interface {
+ Load() (*chart.Chart, 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.Chart, error) {
+ l, err := Loader(name)
+ if err != nil {
+ return nil, err
+ }
+ 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) {
+ c := new(chart.Chart)
+ subcharts := make(map[string][]*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
+ for _, f := range files {
+ c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data})
+ if f.Name == "Chart.yaml" {
+ if c.Metadata == nil {
+ c.Metadata = new(chart.Metadata)
+ }
+ if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
+ return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
+ }
+ // While the documentation says the APIVersion is required, in practice there
+ // are cases where that's not enforced. Since this package set is for v3 charts,
+ // when this function is used v3 is automatically added when not present.
+ if c.Metadata.APIVersion == "" {
+ c.Metadata.APIVersion = chart.APIVersionV3
+ }
+ }
+ }
+ for _, f := range files {
+ switch {
+ case f.Name == "Chart.yaml":
+ // already processed
+ continue
+ case f.Name == "Chart.lock":
+ c.Lock = new(chart.Lock)
+ if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {
+ return c, fmt.Errorf("cannot load Chart.lock: %w", err)
+ }
+ case f.Name == "values.yaml":
+ values, err := LoadValues(bytes.NewReader(f.Data))
+ if err != nil {
+ return c, fmt.Errorf("cannot load values.yaml: %w", err)
+ }
+ c.Values = values
+ case f.Name == "values.schema.json":
+ c.Schema = f.Data
+
+ case strings.HasPrefix(f.Name, "templates/"):
+ c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data})
+ case strings.HasPrefix(f.Name, "charts/"):
+ if filepath.Ext(f.Name) == ".prov" {
+ c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
+ continue
+ }
+
+ fname := strings.TrimPrefix(f.Name, "charts/")
+ cname := strings.SplitN(fname, "/", 2)[0]
+ subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data})
+ default:
+ c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
+ }
+ }
+
+ if c.Metadata == nil {
+ return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck
+ }
+
+ if err := c.Validate(); err != nil {
+ return c, err
+ }
+
+ for n, files := range subcharts {
+ var sc *chart.Chart
+ var err error
+ switch {
+ case strings.IndexAny(n, "_.") == 0:
+ continue
+ case filepath.Ext(n) == ".tgz":
+ file := files[0]
+ if file.Name != n {
+ return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name)
+ }
+ // Untar the chart and add to c.Dependencies
+ sc, err = LoadArchive(bytes.NewBuffer(file.Data))
+ 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))
+ for _, f := range files {
+ parts := strings.SplitN(f.Name, "/", 2)
+ if len(parts) < 2 {
+ continue
+ }
+ f.Name = parts[1]
+ buff = append(buff, f)
+ }
+ sc, err = LoadFiles(buff)
+ }
+
+ if err != nil {
+ return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err)
+ }
+ c.AddDependency(sc)
+ }
+
+ return c, nil
+}
+
+// LoadValues loads values from a reader.
+//
+// The reader is expected to contain one or more YAML documents, the values of which are merged.
+// And the values can be either a chart's default values or a user-supplied values.
+func LoadValues(data io.Reader) (map[string]interface{}, error) {
+ values := map[string]interface{}{}
+ reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
+ for {
+ currentMap := map[string]interface{}{}
+ raw, err := reader.Read()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, fmt.Errorf("error reading yaml document: %w", err)
+ }
+ if err := yaml.Unmarshal(raw, ¤tMap); err != nil {
+ return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err)
+ }
+ values = MergeMaps(values, currentMap)
+ }
+ return values, nil
+}
+
+// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used.
+// If the value is a map, the maps will be merged recursively.
+func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
+ out := make(map[string]interface{}, len(a))
+ maps.Copy(out, a)
+ for k, v := range b {
+ if v, ok := v.(map[string]interface{}); ok {
+ if bv, ok := out[k]; ok {
+ if bv, ok := bv.(map[string]interface{}); ok {
+ out[k] = MergeMaps(bv, v)
+ continue
+ }
+ }
+ }
+ out[k] = v
+ }
+ return out
+}
diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go
new file mode 100644
index 000000000..e770923ff
--- /dev/null
+++ b/internal/chart/v3/loader/load_test.go
@@ -0,0 +1,711 @@
+/*
+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"
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+func TestLoadDir(t *testing.T) {
+ l, err := Loader("testdata/frobnitz")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyFrobnitz(t, c)
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+ verifyDependenciesLock(t, c)
+}
+
+func TestLoadDirWithDevNull(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("test only works on unix systems with /dev/null present")
+ }
+
+ l, err := Loader("testdata/frobnitz_with_dev_null")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ if _, err := l.Load(); err == nil {
+ t.Errorf("packages with an irregular file (/dev/null) should not load")
+ }
+}
+
+func TestLoadDirWithSymlink(t *testing.T) {
+ sym := filepath.Join("..", "LICENSE")
+ link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE")
+
+ if err := os.Symlink(sym, link); err != nil {
+ t.Fatal(err)
+ }
+
+ defer os.Remove(link)
+
+ l, err := Loader("testdata/frobnitz_with_symlink")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyFrobnitz(t, c)
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+ verifyDependenciesLock(t, c)
+}
+
+func TestBomTestData(t *testing.T) {
+ testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"}
+ for _, file := range testFiles {
+ data, err := os.ReadFile("testdata/" + file)
+ if err != nil || !bytes.HasPrefix(data, utf8bom) {
+ t.Errorf("Test file has no BOM or is invalid: testdata/%s", file)
+ }
+ }
+
+ archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz")
+ if err != nil {
+ t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
+ }
+ unzipped, err := gzip.NewReader(bytes.NewReader(archive))
+ if err != nil {
+ t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
+ }
+ defer unzipped.Close()
+ for _, testFile := range testFiles {
+ data := make([]byte, 3)
+ err := unzipped.Reset(bytes.NewReader(archive))
+ if err != nil {
+ t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
+ }
+ tr := tar.NewReader(unzipped)
+ for {
+ file, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
+ }
+ if file != nil && strings.EqualFold(file.Name, testFile) {
+ _, err := tr.Read(data)
+ if err != nil {
+ t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err)
+ } else {
+ break
+ }
+ }
+ }
+ if !bytes.Equal(data, utf8bom) {
+ t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile)
+ }
+ }
+}
+
+func TestLoadDirWithUTFBOM(t *testing.T) {
+ l, err := Loader("testdata/frobnitz_with_bom")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyFrobnitz(t, c)
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+ verifyDependenciesLock(t, c)
+ verifyBomStripped(t, c.Files)
+}
+
+func TestLoadArchiveWithUTFBOM(t *testing.T) {
+ l, err := Loader("testdata/frobnitz_with_bom.tgz")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyFrobnitz(t, c)
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+ verifyDependenciesLock(t, c)
+ verifyBomStripped(t, c.Files)
+}
+
+func TestLoadFile(t *testing.T) {
+ l, err := Loader("testdata/frobnitz-1.2.3.tgz")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyFrobnitz(t, c)
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+}
+
+func TestLoadFiles(t *testing.T) {
+ goodFiles := []*BufferedFile{
+ {
+ Name: "Chart.yaml",
+ Data: []byte(`apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+`),
+ },
+ {
+ Name: "values.yaml",
+ Data: []byte("var: some values"),
+ },
+ {
+ Name: "values.schema.json",
+ Data: []byte("type: Values"),
+ },
+ {
+ Name: "templates/deployment.yaml",
+ Data: []byte("some deployment"),
+ },
+ {
+ Name: "templates/service.yaml",
+ Data: []byte("some service"),
+ },
+ }
+
+ c, err := LoadFiles(goodFiles)
+ if err != nil {
+ t.Errorf("Expected good files to be loaded, got %v", err)
+ }
+
+ if c.Name() != "frobnitz" {
+ t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name())
+ }
+
+ if c.Values["var"] != "some values" {
+ t.Error("Expected chart values to be populated with default values")
+ }
+
+ if len(c.Raw) != 5 {
+ t.Errorf("Expected %d files, got %d", 5, len(c.Raw))
+ }
+
+ if !bytes.Equal(c.Schema, []byte("type: Values")) {
+ t.Error("Expected chart schema to be populated with default values")
+ }
+
+ if len(c.Templates) != 2 {
+ t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
+ }
+
+ if _, err = LoadFiles([]*BufferedFile{}); err == nil {
+ t.Fatal("Expected err to be non-nil")
+ }
+ if err.Error() != "Chart.yaml file is missing" {
+ t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
+ }
+}
+
+// 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{
+ {
+ Name: "requirements.yaml",
+ Data: []byte("dependencies:"),
+ },
+ {
+ Name: "values.yaml",
+ Data: []byte("var: some values"),
+ },
+
+ {
+ Name: "templates/deployment.yaml",
+ Data: []byte("some deployment"),
+ },
+ {
+ Name: "templates/service.yaml",
+ Data: []byte("some service"),
+ },
+ {
+ Name: "Chart.yaml",
+ Data: []byte(`apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+`),
+ },
+ }
+
+ // Capture stderr to make sure message about Chart.yaml handle dependencies
+ // is not present
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("Unable to create pipe: %s", err)
+ }
+ stderr := log.Writer()
+ log.SetOutput(w)
+ defer func() {
+ log.SetOutput(stderr)
+ }()
+
+ _, err = LoadFiles(goodFiles)
+ if err != nil {
+ t.Errorf("Expected good files to be loaded, got %v", err)
+ }
+ w.Close()
+
+ var text bytes.Buffer
+ io.Copy(&text, r)
+ if text.String() != "" {
+ t.Errorf("Expected no message to Stderr, got %s", text.String())
+ }
+
+}
+
+// Packaging the chart on a Windows machine will produce an
+// archive that has \\ as delimiters. Test that we support these archives
+func TestLoadFileBackslash(t *testing.T) {
+ c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyChartFileAndTemplate(t, c, "frobnitz_backslash")
+ verifyChart(t, c)
+ verifyDependencies(t, c)
+}
+
+func TestLoadV3WithReqs(t *testing.T) {
+ l, err := Loader("testdata/frobnitz.v3.reqs")
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ c, err := l.Load()
+ if err != nil {
+ t.Fatalf("Failed to load testdata: %s", err)
+ }
+ verifyDependencies(t, c)
+ verifyDependenciesLock(t, c)
+}
+
+func TestLoadInvalidArchive(t *testing.T) {
+ tmpdir := t.TempDir()
+
+ writeTar := func(filename, internalPath string, body []byte) {
+ dest, err := os.Create(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ zipper := gzip.NewWriter(dest)
+ tw := tar.NewWriter(zipper)
+
+ h := &tar.Header{
+ Name: internalPath,
+ Mode: 0755,
+ Size: int64(len(body)),
+ ModTime: time.Now(),
+ }
+ if err := tw.WriteHeader(h); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := tw.Write(body); err != nil {
+ t.Fatal(err)
+ }
+ tw.Close()
+ zipper.Close()
+ dest.Close()
+ }
+
+ for _, tt := range []struct {
+ chartname string
+ internal string
+ expectError string
+ }{
+ {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"},
+ {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"},
+ {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"},
+ {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"},
+ {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"},
+ {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"},
+ {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"},
+ {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"},
+ {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"},
+ {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"},
+ {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"},
+ {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"},
+
+ // Under special circumstances, this can get normalized to things that look like absolute Windows paths
+ {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"},
+ {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"},
+ {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"},
+ } {
+ illegalChart := filepath.Join(tmpdir, tt.chartname)
+ writeTar(illegalChart, tt.internal, []byte("hello: world"))
+ _, err := Load(illegalChart)
+ if err == nil {
+ t.Fatal("expected error when unpacking illegal files")
+ }
+ if !strings.Contains(err.Error(), tt.expectError) {
+ t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
+ }
+ }
+
+ // Make sure that absolute path gets interpreted as relative
+ illegalChart := filepath.Join(tmpdir, "abs-path.tgz")
+ writeTar(illegalChart, "/Chart.yaml", []byte("hello: world"))
+ _, err := Load(illegalChart)
+ if err.Error() != "validation: chart.metadata.name is required" {
+ t.Error(err)
+ }
+
+ // And just to validate that the above was not spurious
+ illegalChart = filepath.Join(tmpdir, "abs-path2.tgz")
+ writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world"))
+ _, err = Load(illegalChart)
+ if err.Error() != "Chart.yaml file is missing" {
+ t.Errorf("Unexpected error message: %s", err)
+ }
+
+ // Finally, test that drive letter gets stripped off on Windows
+ illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz")
+ writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world"))
+ _, err = Load(illegalChart)
+ if err.Error() != "validation: chart.metadata.name is required" {
+ t.Error(err)
+ }
+}
+
+func TestLoadValues(t *testing.T) {
+ testCases := map[string]struct {
+ data []byte
+ expctedValues map[string]interface{}
+ }{
+ "It should load values correctly": {
+ data: []byte(`
+foo:
+ image: foo:v1
+bar:
+ version: v2
+`),
+ expctedValues: map[string]interface{}{
+ "foo": map[string]interface{}{
+ "image": "foo:v1",
+ },
+ "bar": map[string]interface{}{
+ "version": "v2",
+ },
+ },
+ },
+ "It should load values correctly with multiple documents in one file": {
+ data: []byte(`
+foo:
+ image: foo:v1
+bar:
+ version: v2
+---
+foo:
+ image: foo:v2
+`),
+ expctedValues: map[string]interface{}{
+ "foo": map[string]interface{}{
+ "image": "foo:v2",
+ },
+ "bar": map[string]interface{}{
+ "version": "v2",
+ },
+ },
+ },
+ }
+ for testName, testCase := range testCases {
+ t.Run(testName, func(tt *testing.T) {
+ values, err := LoadValues(bytes.NewReader(testCase.data))
+ if err != nil {
+ tt.Fatal(err)
+ }
+ if !reflect.DeepEqual(values, testCase.expctedValues) {
+ tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values)
+ }
+ })
+ }
+}
+
+func TestMergeValues(t *testing.T) {
+ nestedMap := map[string]interface{}{
+ "foo": "bar",
+ "baz": map[string]string{
+ "cool": "stuff",
+ },
+ }
+ anotherNestedMap := map[string]interface{}{
+ "foo": "bar",
+ "baz": map[string]string{
+ "cool": "things",
+ "awesome": "stuff",
+ },
+ }
+ flatMap := map[string]interface{}{
+ "foo": "bar",
+ "baz": "stuff",
+ }
+ anotherFlatMap := map[string]interface{}{
+ "testing": "fun",
+ }
+
+ testMap := MergeMaps(flatMap, nestedMap)
+ equal := reflect.DeepEqual(testMap, nestedMap)
+ if !equal {
+ t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap)
+ }
+
+ testMap = MergeMaps(nestedMap, flatMap)
+ equal = reflect.DeepEqual(testMap, flatMap)
+ if !equal {
+ t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap)
+ }
+
+ testMap = MergeMaps(nestedMap, anotherNestedMap)
+ equal = reflect.DeepEqual(testMap, anotherNestedMap)
+ if !equal {
+ t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap)
+ }
+
+ testMap = MergeMaps(anotherFlatMap, anotherNestedMap)
+ expectedMap := map[string]interface{}{
+ "testing": "fun",
+ "foo": "bar",
+ "baz": map[string]string{
+ "cool": "things",
+ "awesome": "stuff",
+ },
+ }
+ equal = reflect.DeepEqual(testMap, expectedMap)
+ if !equal {
+ t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap)
+ }
+}
+
+func verifyChart(t *testing.T, c *chart.Chart) {
+ t.Helper()
+ if c.Name() == "" {
+ t.Fatalf("No chart metadata found on %v", c)
+ }
+ t.Logf("Verifying chart %s", c.Name())
+ if len(c.Templates) != 1 {
+ t.Errorf("Expected 1 template, got %d", len(c.Templates))
+ }
+
+ numfiles := 6
+ if len(c.Files) != numfiles {
+ t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
+ for _, n := range c.Files {
+ t.Logf("\t%s", n.Name)
+ }
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies())
+ for _, d := range c.Dependencies() {
+ t.Logf("\tSubchart: %s\n", d.Name())
+ }
+ }
+
+ expect := map[string]map[string]string{
+ "alpine": {
+ "version": "0.1.0",
+ },
+ "mariner": {
+ "version": "4.3.2",
+ },
+ }
+
+ for _, dep := range c.Dependencies() {
+ if dep.Metadata == nil {
+ t.Fatalf("expected metadata on dependency: %v", dep)
+ }
+ exp, ok := expect[dep.Name()]
+ if !ok {
+ t.Fatalf("Unknown dependency %s", dep.Name())
+ }
+ if exp["version"] != dep.Metadata.Version {
+ t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
+ }
+ }
+
+}
+
+func verifyDependencies(t *testing.T, c *chart.Chart) {
+ t.Helper()
+ if len(c.Metadata.Dependencies) != 2 {
+ t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
+ }
+ tests := []*chart.Dependency{
+ {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
+ {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
+ }
+ for i, tt := range tests {
+ d := c.Metadata.Dependencies[i]
+ if d.Name != tt.Name {
+ t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
+ }
+ if d.Version != tt.Version {
+ t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
+ }
+ if d.Repository != tt.Repository {
+ t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
+ }
+ }
+}
+
+func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
+ t.Helper()
+ if len(c.Metadata.Dependencies) != 2 {
+ t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
+ }
+ tests := []*chart.Dependency{
+ {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
+ {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
+ }
+ for i, tt := range tests {
+ d := c.Metadata.Dependencies[i]
+ if d.Name != tt.Name {
+ t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
+ }
+ if d.Version != tt.Version {
+ t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
+ }
+ if d.Repository != tt.Repository {
+ t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
+ }
+ }
+}
+
+func verifyFrobnitz(t *testing.T, c *chart.Chart) {
+ t.Helper()
+ verifyChartFileAndTemplate(t, c, "frobnitz")
+}
+
+func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
+ t.Helper()
+ if c.Metadata == nil {
+ t.Fatal("Metadata is nil")
+ }
+ if c.Name() != name {
+ t.Errorf("Expected %s, got %s", name, c.Name())
+ }
+ if len(c.Templates) != 1 {
+ t.Fatalf("Expected 1 template, got %d", len(c.Templates))
+ }
+ if c.Templates[0].Name != "templates/template.tpl" {
+ t.Errorf("Unexpected template: %s", c.Templates[0].Name)
+ }
+ if len(c.Templates[0].Data) == 0 {
+ t.Error("No template data.")
+ }
+ if len(c.Files) != 6 {
+ t.Fatalf("Expected 6 Files, got %d", len(c.Files))
+ }
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies()))
+ }
+ if len(c.Metadata.Dependencies) != 2 {
+ t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
+ }
+ if len(c.Lock.Dependencies) != 2 {
+ t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
+ }
+
+ for _, dep := range c.Dependencies() {
+ switch dep.Name() {
+ case "mariner":
+ case "alpine":
+ if len(dep.Templates) != 1 {
+ t.Fatalf("Expected 1 template, got %d", len(dep.Templates))
+ }
+ if dep.Templates[0].Name != "templates/alpine-pod.yaml" {
+ t.Errorf("Unexpected template: %s", dep.Templates[0].Name)
+ }
+ if len(dep.Templates[0].Data) == 0 {
+ t.Error("No template data.")
+ }
+ if len(dep.Files) != 1 {
+ t.Fatalf("Expected 1 Files, got %d", len(dep.Files))
+ }
+ if len(dep.Dependencies()) != 2 {
+ t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies()))
+ }
+ default:
+ t.Errorf("Unexpected dependency %s", dep.Name())
+ }
+ }
+}
+
+func verifyBomStripped(t *testing.T, files []*chart.File) {
+ t.Helper()
+ for _, file := range files {
+ if bytes.HasPrefix(file.Data, utf8bom) {
+ t.Errorf("Byte Order Mark still present in processed file %s", file.Name)
+ }
+ }
+}
diff --git a/internal/chart/v3/loader/testdata/LICENSE b/internal/chart/v3/loader/testdata/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/albatross/Chart.yaml b/internal/chart/v3/loader/testdata/albatross/Chart.yaml
new file mode 100644
index 000000000..eeef737ff
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/albatross/Chart.yaml
@@ -0,0 +1,4 @@
+name: albatross
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/albatross/values.yaml b/internal/chart/v3/loader/testdata/albatross/values.yaml
new file mode 100644
index 000000000..3121cd7ce
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/albatross/values.yaml
@@ -0,0 +1,4 @@
+albatross: "true"
+
+global:
+ author: Coleridge
diff --git a/internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz b/internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz
new file mode 100644
index 000000000..de28e4120
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml
new file mode 100644
index 000000000..1b63fc3e2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..21ae20aad
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..3190136b0
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/frobnitz/.helmignore b/internal/chart/v3/loader/testdata/frobnitz/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz/Chart.lock
new file mode 100644
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/loader/testdata/frobnitz/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/Chart.yaml
new file mode 100644
index 000000000..1b63fc3e2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/LICENSE b/internal/chart/v3/loader/testdata/frobnitz/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/README.md b/internal/chart/v3/loader/testdata/frobnitz/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..21ae20aad
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..5c6bc4dcb
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz/icon.svg b/internal/chart/v3/loader/testdata/frobnitz/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz
new file mode 100644
index 000000000..dfbe88a73
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore
new file mode 100755
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock
new file mode 100755
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml
new file mode 100755
index 000000000..6a952e333
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz_backslash
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt
new file mode 100755
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE
new file mode 100755
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/README.md b/internal/chart/v3/loader/testdata/frobnitz_backslash/README.md
new file mode 100755
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me
new file mode 100755
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml
new file mode 100755
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md
new file mode 100755
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml
new file mode 100755
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml
new file mode 100755
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100755
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml
new file mode 100755
index 000000000..0ac5ca6a8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service | quote }}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml
new file mode 100755
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz
new file mode 100755
index 000000000..5c6bc4dcb
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md
new file mode 100755
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/icon.svg b/internal/chart/v3/loader/testdata/frobnitz_backslash/icon.svg
new file mode 100755
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt
new file mode 100755
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl
new file mode 100755
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml
new file mode 100755
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz
new file mode 100644
index 000000000..7f0edc6b2
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore
new file mode 100644
index 000000000..7a4b92da2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock
new file mode 100644
index 000000000..ed43b227f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml
new file mode 100644
index 000000000..924fae6fc
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt
new file mode 100644
index 000000000..77c4e724a
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE
new file mode 100644
index 000000000..c27b00bf2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md
new file mode 100644
index 000000000..e9c40031b
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me
new file mode 100644
index 000000000..a7e3a38b7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..6fe4f411f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md
new file mode 100644
index 000000000..ea7526bee
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..0732c7d7d
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..f690d53c4
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..f3e662a28
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml
new file mode 100644
index 000000000..6b7cb2596
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..5c6bc4dcb
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md
new file mode 100644
index 000000000..816c3e431
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg b/internal/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl
new file mode 100644
index 000000000..bb29c5491
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml
new file mode 100644
index 000000000..c24ceadf9
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock
new file mode 100644
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml
new file mode 100644
index 000000000..1b63fc3e2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..21ae20aad
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..5c6bc4dcb
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null
new file mode 120000
index 000000000..dc1dc0cde
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null
@@ -0,0 +1 @@
+/dev/null
\ No newline at end of file
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock
new file mode 100644
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml
new file mode 100644
index 000000000..1b63fc3e2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..21ae20aad
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ app.kubernetes.io/name: {{.Chart.Name}}
+ helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.9"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..5c6bc4dcb
Binary files /dev/null and b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/loader/testdata/genfrob.sh b/internal/chart/v3/loader/testdata/genfrob.sh
new file mode 100755
index 000000000..eae68906b
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/genfrob.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# Pack the albatross chart into the mariner chart.
+echo "Packing albatross into mariner"
+tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross
+
+echo "Packing mariner into frobnitz"
+tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner
+cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_backslash/charts/
+cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_bom/charts/
+cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_dev_null/charts/
+cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_symlink/charts/
+
+# Pack the frobnitz chart.
+echo "Packing frobnitz"
+tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz
+tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash
+tar --exclude=ignore/* -zcvf frobnitz_with_bom.tgz frobnitz_with_bom
diff --git a/internal/chart/v3/loader/testdata/mariner/Chart.yaml b/internal/chart/v3/loader/testdata/mariner/Chart.yaml
new file mode 100644
index 000000000..4d3eea730
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/mariner/Chart.yaml
@@ -0,0 +1,9 @@
+apiVersion: v3
+name: mariner
+description: A Helm chart for Kubernetes
+version: 4.3.2
+home: ""
+dependencies:
+ - name: albatross
+ repository: https://example.com/mariner/charts
+ version: "0.1.0"
diff --git a/internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz
new file mode 100644
index 000000000..ec7bfbfcf
Binary files /dev/null and b/internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz differ
diff --git a/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl b/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl
new file mode 100644
index 000000000..29c11843a
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl
@@ -0,0 +1 @@
+# This is a placeholder.
diff --git a/internal/chart/v3/loader/testdata/mariner/values.yaml b/internal/chart/v3/loader/testdata/mariner/values.yaml
new file mode 100644
index 000000000..b0ccb0086
--- /dev/null
+++ b/internal/chart/v3/loader/testdata/mariner/values.yaml
@@ -0,0 +1,7 @@
+# Default values for .
+# This is a YAML-formatted file. https://github.com/toml-lang/toml
+# Declare name/value pairs to be passed into your templates.
+# name: "value"
+
+:
+ test: true
diff --git a/internal/chart/v3/metadata.go b/internal/chart/v3/metadata.go
new file mode 100644
index 000000000..4629d571b
--- /dev/null
+++ b/internal/chart/v3/metadata.go
@@ -0,0 +1,178 @@
+/*
+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 v3
+
+import (
+ "path/filepath"
+ "strings"
+ "unicode"
+
+ "github.com/Masterminds/semver/v3"
+)
+
+// Maintainer describes a Chart maintainer.
+type Maintainer struct {
+ // Name is a user name or organization name
+ Name string `json:"name,omitempty"`
+ // Email is an optional email address to contact the named maintainer
+ Email string `json:"email,omitempty"`
+ // URL is an optional URL to an address for the named maintainer
+ URL string `json:"url,omitempty"`
+}
+
+// Validate checks valid data and sanitizes string characters.
+func (m *Maintainer) Validate() error {
+ if m == nil {
+ return ValidationError("maintainers must not contain empty or null nodes")
+ }
+ m.Name = sanitizeString(m.Name)
+ m.Email = sanitizeString(m.Email)
+ m.URL = sanitizeString(m.URL)
+ return nil
+}
+
+// Metadata for a Chart file. This models the structure of a Chart.yaml file.
+type Metadata struct {
+ // The name of the chart. Required.
+ Name string `json:"name,omitempty"`
+ // The URL to a relevant project page, git repo, or contact person
+ Home string `json:"home,omitempty"`
+ // Source is the URL to the source code of this chart
+ Sources []string `json:"sources,omitempty"`
+ // A SemVer 2 conformant version string of the chart. Required.
+ Version string `json:"version,omitempty"`
+ // A one-sentence description of the chart
+ Description string `json:"description,omitempty"`
+ // A list of string keywords
+ Keywords []string `json:"keywords,omitempty"`
+ // A list of name and URL/email address combinations for the maintainer(s)
+ Maintainers []*Maintainer `json:"maintainers,omitempty"`
+ // The URL to an icon file.
+ Icon string `json:"icon,omitempty"`
+ // The API Version of this chart. Required.
+ APIVersion string `json:"apiVersion,omitempty"`
+ // The condition to check to enable chart
+ Condition string `json:"condition,omitempty"`
+ // The tags to check to enable chart
+ Tags string `json:"tags,omitempty"`
+ // The version of the application enclosed inside of this chart.
+ AppVersion string `json:"appVersion,omitempty"`
+ // Whether or not this chart is deprecated
+ Deprecated bool `json:"deprecated,omitempty"`
+ // Annotations are additional mappings uninterpreted by Helm,
+ // made available for inspection by other applications.
+ Annotations map[string]string `json:"annotations,omitempty"`
+ // KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
+ KubeVersion string `json:"kubeVersion,omitempty"`
+ // Dependencies are a list of dependencies for a chart.
+ Dependencies []*Dependency `json:"dependencies,omitempty"`
+ // Specifies the chart type: application or library
+ Type string `json:"type,omitempty"`
+}
+
+// Validate checks the metadata for known issues and sanitizes string
+// characters.
+func (md *Metadata) Validate() error {
+ if md == nil {
+ return ValidationError("chart.metadata is required")
+ }
+
+ md.Name = sanitizeString(md.Name)
+ md.Description = sanitizeString(md.Description)
+ md.Home = sanitizeString(md.Home)
+ md.Icon = sanitizeString(md.Icon)
+ md.Condition = sanitizeString(md.Condition)
+ md.Tags = sanitizeString(md.Tags)
+ md.AppVersion = sanitizeString(md.AppVersion)
+ md.KubeVersion = sanitizeString(md.KubeVersion)
+ for i := range md.Sources {
+ md.Sources[i] = sanitizeString(md.Sources[i])
+ }
+ for i := range md.Keywords {
+ md.Keywords[i] = sanitizeString(md.Keywords[i])
+ }
+
+ if md.APIVersion == "" {
+ return ValidationError("chart.metadata.apiVersion is required")
+ }
+ if md.Name == "" {
+ return ValidationError("chart.metadata.name is required")
+ }
+
+ if md.Name != filepath.Base(md.Name) {
+ return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
+ }
+
+ if md.Version == "" {
+ return ValidationError("chart.metadata.version is required")
+ }
+ if !isValidSemver(md.Version) {
+ return ValidationErrorf("chart.metadata.version %q is invalid", md.Version)
+ }
+ if !isValidChartType(md.Type) {
+ return ValidationError("chart.metadata.type must be application or library")
+ }
+
+ for _, m := range md.Maintainers {
+ if err := m.Validate(); err != nil {
+ return err
+ }
+ }
+
+ // Aliases need to be validated here to make sure that the alias name does
+ // not contain any illegal characters.
+ dependencies := map[string]*Dependency{}
+ for _, dependency := range md.Dependencies {
+ if err := dependency.Validate(); err != nil {
+ return err
+ }
+ key := dependency.Name
+ if dependency.Alias != "" {
+ key = dependency.Alias
+ }
+ if dependencies[key] != nil {
+ return ValidationErrorf("more than one dependency with name or alias %q", key)
+ }
+ dependencies[key] = dependency
+ }
+ return nil
+}
+
+func isValidChartType(in string) bool {
+ switch in {
+ case "", "application", "library":
+ return true
+ }
+ return false
+}
+
+func isValidSemver(v string) bool {
+ _, err := semver.NewVersion(v)
+ return err == nil
+}
+
+// sanitizeString normalize spaces and removes non-printable characters.
+func sanitizeString(str string) string {
+ return strings.Map(func(r rune) rune {
+ if unicode.IsSpace(r) {
+ return ' '
+ }
+ if unicode.IsPrint(r) {
+ return r
+ }
+ return -1
+ }, str)
+}
diff --git a/internal/chart/v3/metadata_test.go b/internal/chart/v3/metadata_test.go
new file mode 100644
index 000000000..596a03695
--- /dev/null
+++ b/internal/chart/v3/metadata_test.go
@@ -0,0 +1,201 @@
+/*
+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 v3
+
+import (
+ "testing"
+)
+
+func TestValidate(t *testing.T) {
+ tests := []struct {
+ name string
+ md *Metadata
+ err error
+ }{
+ {
+ "chart without metadata",
+ nil,
+ ValidationError("chart.metadata is required"),
+ },
+ {
+ "chart without apiVersion",
+ &Metadata{Name: "test", Version: "1.0"},
+ ValidationError("chart.metadata.apiVersion is required"),
+ },
+ {
+ "chart without name",
+ &Metadata{APIVersion: "v3", Version: "1.0"},
+ ValidationError("chart.metadata.name is required"),
+ },
+ {
+ "chart without name",
+ &Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"},
+ ValidationError("chart.metadata.name \"../../test\" is invalid"),
+ },
+ {
+ "chart without version",
+ &Metadata{Name: "test", APIVersion: "v3"},
+ ValidationError("chart.metadata.version is required"),
+ },
+ {
+ "chart with bad type",
+ &Metadata{Name: "test", APIVersion: "v3", Version: "1.0", Type: "test"},
+ ValidationError("chart.metadata.type must be application or library"),
+ },
+ {
+ "chart without dependency",
+ &Metadata{Name: "test", APIVersion: "v3", Version: "1.0", Type: "application"},
+ nil,
+ },
+ {
+ "dependency with valid alias",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "dependency", Alias: "legal-alias"},
+ },
+ },
+ nil,
+ },
+ {
+ "dependency with bad characters in alias",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "bad", Alias: "illegal alias"},
+ },
+ },
+ ValidationError("dependency \"bad\" has disallowed characters in the alias"),
+ },
+ {
+ "same dependency twice",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "foo", Alias: ""},
+ {Name: "foo", Alias: ""},
+ },
+ },
+ ValidationError("more than one dependency with name or alias \"foo\""),
+ },
+ {
+ "two dependencies with alias from second dependency shadowing first one",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "foo", Alias: ""},
+ {Name: "bar", Alias: "foo"},
+ },
+ },
+ ValidationError("more than one dependency with name or alias \"foo\""),
+ },
+ {
+ // this case would make sense and could work in future versions of Helm, currently template rendering would
+ // result in undefined behaviour
+ "same dependency twice with different version",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "foo", Alias: "", Version: "1.2.3"},
+ {Name: "foo", Alias: "", Version: "1.0.0"},
+ },
+ },
+ ValidationError("more than one dependency with name or alias \"foo\""),
+ },
+ {
+ // this case would make sense and could work in future versions of Helm, currently template rendering would
+ // result in undefined behaviour
+ "two dependencies with same name but different repos",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ {Name: "foo", Repository: "repo-0"},
+ {Name: "foo", Repository: "repo-1"},
+ },
+ },
+ ValidationError("more than one dependency with name or alias \"foo\""),
+ },
+ {
+ "dependencies has nil",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Dependencies: []*Dependency{
+ nil,
+ },
+ },
+ ValidationError("dependencies must not contain empty or null nodes"),
+ },
+ {
+ "maintainer not empty",
+ &Metadata{
+ Name: "test",
+ APIVersion: "v3",
+ Version: "1.0",
+ Type: "application",
+ Maintainers: []*Maintainer{
+ nil,
+ },
+ },
+ ValidationError("maintainers must not contain empty or null nodes"),
+ },
+ {
+ "version invalid",
+ &Metadata{APIVersion: "3", Name: "test", Version: "1.2.3.4"},
+ ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"),
+ },
+ }
+
+ for _, tt := range tests {
+ result := tt.md.Validate()
+ if result != tt.err {
+ t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name)
+ }
+ }
+}
+
+func TestValidate_sanitize(t *testing.T) {
+ md := &Metadata{APIVersion: "3", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}}
+ if err := md.Validate(); err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if md.Description != "description test" {
+ t.Fatalf("description was not sanitized: %q", md.Description)
+ }
+ if md.Maintainers[0].Name != " " {
+ t.Fatal("maintainer name was not sanitized")
+ }
+}
diff --git a/internal/chart/v3/util/capabilities.go b/internal/chart/v3/util/capabilities.go
new file mode 100644
index 000000000..23b6d46fa
--- /dev/null
+++ b/internal/chart/v3/util/capabilities.go
@@ -0,0 +1,122 @@
+/*
+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 util
+
+import (
+ "fmt"
+ "slices"
+ "strconv"
+
+ "github.com/Masterminds/semver/v3"
+ "k8s.io/client-go/kubernetes/scheme"
+
+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
+
+ helmversion "helm.sh/helm/v4/internal/version"
+)
+
+var (
+ // The Kubernetes version can be set by LDFLAGS. In order to do that the value
+ // must be a string.
+ k8sVersionMajor = "1"
+ k8sVersionMinor = "20"
+
+ // DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
+ DefaultVersionSet = allKnownVersions()
+
+ // DefaultCapabilities is the default set of capabilities.
+ DefaultCapabilities = &Capabilities{
+ KubeVersion: KubeVersion{
+ Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
+ Major: k8sVersionMajor,
+ Minor: k8sVersionMinor,
+ },
+ APIVersions: DefaultVersionSet,
+ HelmVersion: helmversion.Get(),
+ }
+)
+
+// Capabilities describes the capabilities of the Kubernetes cluster.
+type Capabilities struct {
+ // KubeVersion is the Kubernetes version.
+ KubeVersion KubeVersion
+ // APIVersions are supported Kubernetes API versions.
+ APIVersions VersionSet
+ // HelmVersion is the build information for this helm version
+ HelmVersion helmversion.BuildInfo
+}
+
+func (capabilities *Capabilities) Copy() *Capabilities {
+ return &Capabilities{
+ KubeVersion: capabilities.KubeVersion,
+ APIVersions: capabilities.APIVersions,
+ HelmVersion: capabilities.HelmVersion,
+ }
+}
+
+// KubeVersion is the Kubernetes version.
+type KubeVersion struct {
+ Version string // Kubernetes version
+ Major string // Kubernetes major version
+ Minor string // Kubernetes minor version
+}
+
+// String implements fmt.Stringer
+func (kv *KubeVersion) String() string { return kv.Version }
+
+// GitVersion returns the Kubernetes version string.
+//
+// Deprecated: use KubeVersion.Version.
+func (kv *KubeVersion) GitVersion() string { return kv.Version }
+
+// ParseKubeVersion parses kubernetes version from string
+func ParseKubeVersion(version string) (*KubeVersion, error) {
+ sv, err := semver.NewVersion(version)
+ if err != nil {
+ return nil, err
+ }
+ return &KubeVersion{
+ Version: "v" + sv.String(),
+ Major: strconv.FormatUint(sv.Major(), 10),
+ Minor: strconv.FormatUint(sv.Minor(), 10),
+ }, nil
+}
+
+// VersionSet is a set of Kubernetes API versions.
+type VersionSet []string
+
+// Has returns true if the version string is in the set.
+//
+// vs.Has("apps/v1")
+func (v VersionSet) Has(apiVersion string) bool {
+ return slices.Contains(v, apiVersion)
+}
+
+func allKnownVersions() VersionSet {
+ // We should register the built in extension APIs as well so CRDs are
+ // supported in the default version set. This has caused problems with `helm
+ // template` in the past, so let's be safe
+ apiextensionsv1beta1.AddToScheme(scheme.Scheme)
+ apiextensionsv1.AddToScheme(scheme.Scheme)
+
+ groups := scheme.Scheme.PrioritizedVersionsAllGroups()
+ vs := make(VersionSet, 0, len(groups))
+ for _, gv := range groups {
+ vs = append(vs, gv.String())
+ }
+ return vs
+}
diff --git a/internal/chart/v3/util/capabilities_test.go b/internal/chart/v3/util/capabilities_test.go
new file mode 100644
index 000000000..aa9be9db8
--- /dev/null
+++ b/internal/chart/v3/util/capabilities_test.go
@@ -0,0 +1,84 @@
+/*
+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 util
+
+import (
+ "testing"
+)
+
+func TestVersionSet(t *testing.T) {
+ vs := VersionSet{"v1", "apps/v1"}
+ if d := len(vs); d != 2 {
+ t.Errorf("Expected 2 versions, got %d", d)
+ }
+
+ if !vs.Has("apps/v1") {
+ t.Error("Expected to find apps/v1")
+ }
+
+ if vs.Has("Spanish/inquisition") {
+ t.Error("No one expects the Spanish/inquisition")
+ }
+}
+
+func TestDefaultVersionSet(t *testing.T) {
+ if !DefaultVersionSet.Has("v1") {
+ t.Error("Expected core v1 version set")
+ }
+}
+
+func TestDefaultCapabilities(t *testing.T) {
+ kv := DefaultCapabilities.KubeVersion
+ if kv.String() != "v1.20.0" {
+ t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String())
+ }
+ if kv.Version != "v1.20.0" {
+ t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version)
+ }
+ if kv.GitVersion() != "v1.20.0" {
+ t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version)
+ }
+ if kv.Major != "1" {
+ t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major)
+ }
+ if kv.Minor != "20" {
+ t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
+ }
+}
+
+func TestDefaultCapabilitiesHelmVersion(t *testing.T) {
+ hv := DefaultCapabilities.HelmVersion
+
+ if hv.Version != "v4.0" {
+ t.Errorf("Expected default HelmVersion to be v4.0, got %q", hv.Version)
+ }
+}
+
+func TestParseKubeVersion(t *testing.T) {
+ kv, err := ParseKubeVersion("v1.16.0")
+ if err != nil {
+ t.Errorf("Expected v1.16.0 to parse successfully")
+ }
+ if kv.Version != "v1.16.0" {
+ t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String())
+ }
+ if kv.Major != "1" {
+ t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major)
+ }
+ if kv.Minor != "16" {
+ t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor)
+ }
+}
diff --git a/internal/chart/v3/util/chartfile.go b/internal/chart/v3/util/chartfile.go
new file mode 100644
index 000000000..25271e1cf
--- /dev/null
+++ b/internal/chart/v3/util/chartfile.go
@@ -0,0 +1,96 @@
+/*
+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 util
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// LoadChartfile loads a Chart.yaml file into a *chart.Metadata.
+func LoadChartfile(filename string) (*chart.Metadata, error) {
+ b, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ y := new(chart.Metadata)
+ err = yaml.Unmarshal(b, y)
+ return y, err
+}
+
+// StrictLoadChartfile loads a Chart.yaml into a *chart.Metadata using a strict unmarshaling
+func StrictLoadChartfile(filename string) (*chart.Metadata, error) {
+ b, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ y := new(chart.Metadata)
+ err = yaml.UnmarshalStrict(b, y)
+ return y, err
+}
+
+// SaveChartfile saves the given metadata as a Chart.yaml file at the given path.
+//
+// 'filename' should be the complete path and filename ('foo/Chart.yaml')
+func SaveChartfile(filename string, cf *chart.Metadata) error {
+ out, err := yaml.Marshal(cf)
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(filename, out, 0644)
+}
+
+// IsChartDir validate a chart directory.
+//
+// Checks for a valid Chart.yaml.
+func IsChartDir(dirName string) (bool, error) {
+ if fi, err := os.Stat(dirName); err != nil {
+ return false, err
+ } else if !fi.IsDir() {
+ return false, fmt.Errorf("%q is not a directory", dirName)
+ }
+
+ chartYaml := filepath.Join(dirName, ChartfileName)
+ if _, err := os.Stat(chartYaml); errors.Is(err, fs.ErrNotExist) {
+ return false, fmt.Errorf("no %s exists in directory %q", ChartfileName, dirName)
+ }
+
+ chartYamlContent, err := os.ReadFile(chartYaml)
+ if err != nil {
+ return false, fmt.Errorf("cannot read %s in directory %q", ChartfileName, dirName)
+ }
+
+ chartContent := new(chart.Metadata)
+ if err := yaml.Unmarshal(chartYamlContent, &chartContent); err != nil {
+ return false, err
+ }
+ if chartContent == nil {
+ return false, fmt.Errorf("chart metadata (%s) missing", ChartfileName)
+ }
+ if chartContent.Name == "" {
+ return false, fmt.Errorf("invalid chart (%s): name must not be empty", ChartfileName)
+ }
+
+ return true, nil
+}
diff --git a/internal/chart/v3/util/chartfile_test.go b/internal/chart/v3/util/chartfile_test.go
new file mode 100644
index 000000000..c3d19c381
--- /dev/null
+++ b/internal/chart/v3/util/chartfile_test.go
@@ -0,0 +1,117 @@
+/*
+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 util
+
+import (
+ "testing"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+const testfile = "testdata/chartfiletest.yaml"
+
+func TestLoadChartfile(t *testing.T) {
+ f, err := LoadChartfile(testfile)
+ if err != nil {
+ t.Errorf("Failed to open %s: %s", testfile, err)
+ return
+ }
+ verifyChartfile(t, f, "frobnitz")
+}
+
+func verifyChartfile(t *testing.T, f *chart.Metadata, name string) {
+ t.Helper()
+ if f == nil { //nolint:staticcheck
+ t.Fatal("Failed verifyChartfile because f is nil")
+ }
+
+ if f.Name != name {
+ t.Errorf("Expected %s, got %s", name, f.Name)
+ }
+
+ if f.Description != "This is a frobnitz." {
+ t.Errorf("Unexpected description %q", f.Description)
+ }
+
+ if f.Version != "1.2.3" {
+ t.Errorf("Unexpected version %q", f.Version)
+ }
+
+ if len(f.Maintainers) != 2 {
+ t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers))
+ }
+
+ if f.Maintainers[0].Name != "The Helm Team" {
+ t.Errorf("Unexpected maintainer name.")
+ }
+
+ if f.Maintainers[1].Email != "nobody@example.com" {
+ t.Errorf("Unexpected maintainer email.")
+ }
+
+ if len(f.Sources) != 1 {
+ t.Fatalf("Unexpected number of sources")
+ }
+
+ if f.Sources[0] != "https://example.com/foo/bar" {
+ t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources)
+ }
+
+ if f.Home != "http://example.com" {
+ t.Error("Unexpected home.")
+ }
+
+ if f.Icon != "https://example.com/64x64.png" {
+ t.Errorf("Unexpected icon: %q", f.Icon)
+ }
+
+ if len(f.Keywords) != 3 {
+ t.Error("Unexpected keywords")
+ }
+
+ if len(f.Annotations) != 2 {
+ t.Fatalf("Unexpected annotations")
+ }
+
+ if want, got := "extravalue", f.Annotations["extrakey"]; want != got {
+ t.Errorf("Want %q, but got %q", want, got)
+ }
+
+ if want, got := "anothervalue", f.Annotations["anotherkey"]; want != got {
+ t.Errorf("Want %q, but got %q", want, got)
+ }
+
+ kk := []string{"frobnitz", "sprocket", "dodad"}
+ for i, k := range f.Keywords {
+ if kk[i] != k {
+ t.Errorf("Expected %q, got %q", kk[i], k)
+ }
+ }
+}
+
+func TestIsChartDir(t *testing.T) {
+ validChartDir, err := IsChartDir("testdata/frobnitz")
+ if !validChartDir {
+ t.Errorf("unexpected error while reading chart-directory: (%v)", err)
+ return
+ }
+ validChartDir, err = IsChartDir("testdata")
+ if validChartDir || err == nil {
+ t.Errorf("expected error but did not get any")
+ return
+ }
+}
diff --git a/internal/chart/v3/util/coalesce.go b/internal/chart/v3/util/coalesce.go
new file mode 100644
index 000000000..caea2e119
--- /dev/null
+++ b/internal/chart/v3/util/coalesce.go
@@ -0,0 +1,308 @@
+/*
+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 util
+
+import (
+ "fmt"
+ "log"
+ "maps"
+
+ "github.com/mitchellh/copystructure"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+func concatPrefix(a, b string) string {
+ if a == "" {
+ return b
+ }
+ return fmt.Sprintf("%s.%s", a, b)
+}
+
+// CoalesceValues coalesces all of the values in a chart (and its subcharts).
+//
+// Values are coalesced together using the following rules:
+//
+// - Values in a higher level chart always override values in a lower-level
+// dependency chart
+// - Scalar values and arrays are replaced, maps are merged
+// - A chart has access to all of the variables for it, as well as all of
+// the values destined for its dependencies.
+func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
+ valsCopy, err := copyValues(vals)
+ if err != nil {
+ return vals, err
+ }
+ return coalesce(log.Printf, chrt, valsCopy, "", false)
+}
+
+// MergeValues is used to merge the values in a chart and its subcharts. This
+// is different from Coalescing as nil/null values are preserved.
+//
+// Values are coalesced together using the following rules:
+//
+// - Values in a higher level chart always override values in a lower-level
+// dependency chart
+// - Scalar values and arrays are replaced, maps are merged
+// - A chart has access to all of the variables for it, as well as all of
+// the values destined for its dependencies.
+//
+// Retaining Nils is useful when processes early in a Helm action or business
+// logic need to retain them for when Coalescing will happen again later in the
+// business logic.
+func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
+ valsCopy, err := copyValues(vals)
+ if err != nil {
+ return vals, err
+ }
+ return coalesce(log.Printf, chrt, valsCopy, "", true)
+}
+
+func copyValues(vals map[string]interface{}) (Values, error) {
+ v, err := copystructure.Copy(vals)
+ if err != nil {
+ return vals, err
+ }
+
+ valsCopy := v.(map[string]interface{})
+ // if we have an empty map, make sure it is initialized
+ if valsCopy == nil {
+ valsCopy = make(map[string]interface{})
+ }
+
+ return valsCopy, nil
+}
+
+type printFn func(format string, v ...interface{})
+
+// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
+//
+// This is a helper function for CoalesceValues and MergeValues.
+//
+// Note, the merge argument specifies whether this is being used by MergeValues
+// or CoalesceValues. Coalescing removes null values and their keys in some
+// situations while merging keeps the null values.
+func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
+ coalesceValues(printf, ch, dest, prefix, merge)
+ return coalesceDeps(printf, ch, dest, prefix, merge)
+}
+
+// coalesceDeps coalesces the dependencies of the given chart.
+func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
+ for _, subchart := range chrt.Dependencies() {
+ if c, ok := dest[subchart.Name()]; !ok {
+ // If dest doesn't already have the key, create it.
+ dest[subchart.Name()] = make(map[string]interface{})
+ } else if !istable(c) {
+ return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Name(), c)
+ }
+ if dv, ok := dest[subchart.Name()]; ok {
+ dvmap := dv.(map[string]interface{})
+ subPrefix := concatPrefix(prefix, chrt.Metadata.Name)
+ // Get globals out of dest and merge them into dvmap.
+ coalesceGlobals(printf, dvmap, dest, subPrefix, merge)
+ // Now coalesce the rest of the values.
+ var err error
+ dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge)
+ if err != nil {
+ return dest, err
+ }
+ }
+ }
+ return dest, nil
+}
+
+// coalesceGlobals copies the globals out of src and merges them into dest.
+//
+// For convenience, returns dest.
+func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) {
+ var dg, sg map[string]interface{}
+
+ if destglob, ok := dest[GlobalKey]; !ok {
+ dg = make(map[string]interface{})
+ } else if dg, ok = destglob.(map[string]interface{}); !ok {
+ printf("warning: skipping globals because destination %s is not a table.", GlobalKey)
+ return
+ }
+
+ if srcglob, ok := src[GlobalKey]; !ok {
+ sg = make(map[string]interface{})
+ } else if sg, ok = srcglob.(map[string]interface{}); !ok {
+ printf("warning: skipping globals because source %s is not a table.", GlobalKey)
+ return
+ }
+
+ // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
+ // reverses that decision. It may somehow be possible to introduce a loop
+ // here, but I haven't found a way. So for the time being, let's allow
+ // tables in globals.
+ for key, val := range sg {
+ if istable(val) {
+ vv := copyMap(val.(map[string]interface{}))
+ if destv, ok := dg[key]; !ok {
+ // Here there is no merge. We're just adding.
+ dg[key] = vv
+ } else {
+ if destvmap, ok := destv.(map[string]interface{}); !ok {
+ printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
+ } else {
+ // Basically, we reverse order of coalesce here to merge
+ // top-down.
+ subPrefix := concatPrefix(prefix, key)
+ // In this location coalesceTablesFullKey should always have
+ // merge set to true. The output of coalesceGlobals is run
+ // through coalesce where any nils will be removed.
+ coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true)
+ dg[key] = vv
+ }
+ }
+ } else if dv, ok := dg[key]; ok && istable(dv) {
+ // It's not clear if this condition can actually ever trigger.
+ printf("key %s is table. Skipping", key)
+ } else {
+ // TODO: Do we need to do any additional checking on the value?
+ dg[key] = val
+ }
+ }
+ dest[GlobalKey] = dg
+}
+
+func copyMap(src map[string]interface{}) map[string]interface{} {
+ m := make(map[string]interface{}, len(src))
+ maps.Copy(m, src)
+ return m
+}
+
+// coalesceValues builds up a values map for a particular chart.
+//
+// Values in v will override the values in the chart.
+func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) {
+ subPrefix := concatPrefix(prefix, c.Metadata.Name)
+
+ // Using c.Values directly when coalescing a table can cause problems where
+ // the original c.Values is altered. Creating a deep copy stops the problem.
+ // This section is fault-tolerant as there is no ability to return an error.
+ valuesCopy, err := copystructure.Copy(c.Values)
+ var vc map[string]interface{}
+ var ok bool
+ if err != nil {
+ // If there is an error something is wrong with copying c.Values it
+ // means there is a problem in the deep copying package or something
+ // wrong with c.Values. In this case we will use c.Values and report
+ // an error.
+ printf("warning: unable to copy values, err: %s", err)
+ vc = c.Values
+ } else {
+ vc, ok = valuesCopy.(map[string]interface{})
+ if !ok {
+ // c.Values has a map[string]interface{} structure. If the copy of
+ // it cannot be treated as map[string]interface{} there is something
+ // strangely wrong. Log it and use c.Values
+ printf("warning: unable to convert values copy to values type")
+ vc = c.Values
+ }
+ }
+
+ for key, val := range vc {
+ if value, ok := v[key]; ok {
+ if value == nil && !merge {
+ // When the YAML value is null and we are coalescing instead of
+ // merging, we remove the value's key.
+ // This allows Helm's various sources of values (value files or --set) to
+ // remove incompatible keys from any previous chart, file, or set values.
+ delete(v, key)
+ } else if dest, ok := value.(map[string]interface{}); ok {
+ // if v[key] is a table, merge nv's val table into v[key].
+ src, ok := val.(map[string]interface{})
+ if !ok {
+ // If the original value is nil, there is nothing to coalesce, so we don't print
+ // the warning
+ if val != nil {
+ printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key)
+ }
+ } else {
+ // If the key is a child chart, coalesce tables with Merge set to true
+ merge := childChartMergeTrue(c, key, merge)
+
+ // Because v has higher precedence than nv, dest values override src
+ // values.
+ coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
+ }
+ }
+ } else {
+ // If the key is not in v, copy it from nv.
+ v[key] = val
+ }
+ }
+}
+
+func childChartMergeTrue(chrt *chart.Chart, key string, merge bool) bool {
+ for _, subchart := range chrt.Dependencies() {
+ if subchart.Name() == key {
+ return true
+ }
+ }
+ return merge
+}
+
+// CoalesceTables merges a source map into a destination map.
+//
+// dest is considered authoritative.
+func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
+ return coalesceTablesFullKey(log.Printf, dst, src, "", false)
+}
+
+func MergeTables(dst, src map[string]interface{}) map[string]interface{} {
+ return coalesceTablesFullKey(log.Printf, dst, src, "", true)
+}
+
+// coalesceTablesFullKey merges a source map into a destination map.
+//
+// dest is considered authoritative.
+func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} {
+ // When --reuse-values is set but there are no modifications yet, return new values
+ if src == nil {
+ return dst
+ }
+ if dst == nil {
+ return src
+ }
+ for key, val := range dst {
+ if val == nil {
+ src[key] = nil
+ }
+ }
+ // Because dest has higher precedence than src, dest values override src
+ // values.
+ for key, val := range src {
+ fullkey := concatPrefix(prefix, key)
+ if dv, ok := dst[key]; ok && !merge && dv == nil {
+ delete(dst, key)
+ } else if !ok {
+ dst[key] = val
+ } else if istable(val) {
+ if istable(dv) {
+ coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge)
+ } else {
+ printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val)
+ }
+ } else if istable(dv) && val != nil {
+ printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val)
+ }
+ }
+ return dst
+}
diff --git a/internal/chart/v3/util/coalesce_test.go b/internal/chart/v3/util/coalesce_test.go
new file mode 100644
index 000000000..4770b601d
--- /dev/null
+++ b/internal/chart/v3/util/coalesce_test.go
@@ -0,0 +1,723 @@
+/*
+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 util
+
+import (
+ "encoding/json"
+ "fmt"
+ "maps"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362
+var testCoalesceValuesYaml = []byte(`
+top: yup
+bottom: null
+right: Null
+left: NULL
+front: ~
+back: ""
+nested:
+ boat: null
+
+global:
+ name: Ishmael
+ subject: Queequeg
+ nested:
+ boat: true
+
+pequod:
+ boat: null
+ global:
+ name: Stinky
+ harpooner: Tashtego
+ nested:
+ boat: false
+ sail: true
+ foo2: null
+ ahab:
+ scope: whale
+ boat: null
+ nested:
+ foo: true
+ boat: null
+ object: null
+`)
+
+func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart {
+ c.AddDependency(deps...)
+ return c
+}
+
+func TestCoalesceValues(t *testing.T) {
+ is := assert.New(t)
+
+ c := withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "moby"},
+ Values: map[string]interface{}{
+ "back": "exists",
+ "bottom": "exists",
+ "front": "exists",
+ "left": "exists",
+ "name": "moby",
+ "nested": map[string]interface{}{"boat": true},
+ "override": "bad",
+ "right": "exists",
+ "scope": "moby",
+ "top": "nope",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l0": "moby"},
+ },
+ "pequod": map[string]interface{}{
+ "boat": "maybe",
+ "ahab": map[string]interface{}{
+ "boat": "maybe",
+ "nested": map[string]interface{}{"boat": "maybe"},
+ },
+ },
+ },
+ },
+ withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "pequod"},
+ Values: map[string]interface{}{
+ "name": "pequod",
+ "scope": "pequod",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l1": "pequod"},
+ },
+ "boat": false,
+ "ahab": map[string]interface{}{
+ "boat": false,
+ "nested": map[string]interface{}{"boat": false},
+ },
+ },
+ },
+ &chart.Chart{
+ Metadata: &chart.Metadata{Name: "ahab"},
+ Values: map[string]interface{}{
+ "global": map[string]interface{}{
+ "nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"},
+ "nested2": map[string]interface{}{"l2": "ahab"},
+ },
+ "scope": "ahab",
+ "name": "ahab",
+ "boat": true,
+ "nested": map[string]interface{}{"foo": false, "boat": true},
+ "object": map[string]interface{}{"foo": "bar"},
+ },
+ },
+ ),
+ &chart.Chart{
+ Metadata: &chart.Metadata{Name: "spouter"},
+ Values: map[string]interface{}{
+ "scope": "spouter",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l1": "spouter"},
+ },
+ },
+ },
+ )
+
+ vals, err := ReadValues(testCoalesceValuesYaml)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // taking a copy of the values before passing it
+ // to CoalesceValues as argument, so that we can
+ // use it for asserting later
+ valsCopy := make(Values, len(vals))
+ maps.Copy(valsCopy, vals)
+
+ v, err := CoalesceValues(c, vals)
+ if err != nil {
+ t.Fatal(err)
+ }
+ j, _ := json.MarshalIndent(v, "", " ")
+ t.Logf("Coalesced Values: %s", string(j))
+
+ tests := []struct {
+ tpl string
+ expect string
+ }{
+ {"{{.top}}", "yup"},
+ {"{{.back}}", ""},
+ {"{{.name}}", "moby"},
+ {"{{.global.name}}", "Ishmael"},
+ {"{{.global.subject}}", "Queequeg"},
+ {"{{.global.harpooner}}", ""},
+ {"{{.pequod.name}}", "pequod"},
+ {"{{.pequod.ahab.name}}", "ahab"},
+ {"{{.pequod.ahab.scope}}", "whale"},
+ {"{{.pequod.ahab.nested.foo}}", "true"},
+ {"{{.pequod.ahab.global.name}}", "Ishmael"},
+ {"{{.pequod.ahab.global.nested.foo}}", "bar"},
+ {"{{.pequod.ahab.global.nested.foo2}}", ""},
+ {"{{.pequod.ahab.global.subject}}", "Queequeg"},
+ {"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
+ {"{{.pequod.global.name}}", "Ishmael"},
+ {"{{.pequod.global.nested.foo}}", ""},
+ {"{{.pequod.global.subject}}", "Queequeg"},
+ {"{{.spouter.global.name}}", "Ishmael"},
+ {"{{.spouter.global.harpooner}}", ""},
+
+ {"{{.global.nested.boat}}", "true"},
+ {"{{.pequod.global.nested.boat}}", "true"},
+ {"{{.spouter.global.nested.boat}}", "true"},
+ {"{{.pequod.global.nested.sail}}", "true"},
+ {"{{.spouter.global.nested.sail}}", ""},
+
+ {"{{.global.nested2.l0}}", "moby"},
+ {"{{.global.nested2.l1}}", ""},
+ {"{{.global.nested2.l2}}", ""},
+ {"{{.pequod.global.nested2.l0}}", "moby"},
+ {"{{.pequod.global.nested2.l1}}", "pequod"},
+ {"{{.pequod.global.nested2.l2}}", ""},
+ {"{{.pequod.ahab.global.nested2.l0}}", "moby"},
+ {"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
+ {"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
+ {"{{.spouter.global.nested2.l0}}", "moby"},
+ {"{{.spouter.global.nested2.l1}}", "spouter"},
+ {"{{.spouter.global.nested2.l2}}", ""},
+ }
+
+ for _, tt := range tests {
+ if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
+ t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
+ }
+ }
+
+ nullKeys := []string{"bottom", "right", "left", "front"}
+ for _, nullKey := range nullKeys {
+ if _, ok := v[nullKey]; ok {
+ t.Errorf("Expected key %q to be removed, still present", nullKey)
+ }
+ }
+
+ if _, ok := v["nested"].(map[string]interface{})["boat"]; ok {
+ t.Error("Expected nested boat key to be removed, still present")
+ }
+
+ subchart := v["pequod"].(map[string]interface{})
+ if _, ok := subchart["boat"]; ok {
+ t.Error("Expected subchart boat key to be removed, still present")
+ }
+
+ subsubchart := subchart["ahab"].(map[string]interface{})
+ if _, ok := subsubchart["boat"]; ok {
+ t.Error("Expected sub-subchart ahab boat key to be removed, still present")
+ }
+
+ if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok {
+ t.Error("Expected sub-subchart nested boat key to be removed, still present")
+ }
+
+ if _, ok := subsubchart["object"]; ok {
+ t.Error("Expected sub-subchart object map to be removed, still present")
+ }
+
+ // CoalesceValues should not mutate the passed arguments
+ is.Equal(valsCopy, vals)
+}
+
+func TestMergeValues(t *testing.T) {
+ is := assert.New(t)
+
+ c := withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "moby"},
+ Values: map[string]interface{}{
+ "back": "exists",
+ "bottom": "exists",
+ "front": "exists",
+ "left": "exists",
+ "name": "moby",
+ "nested": map[string]interface{}{"boat": true},
+ "override": "bad",
+ "right": "exists",
+ "scope": "moby",
+ "top": "nope",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l0": "moby"},
+ },
+ },
+ },
+ withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "pequod"},
+ Values: map[string]interface{}{
+ "name": "pequod",
+ "scope": "pequod",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l1": "pequod"},
+ },
+ },
+ },
+ &chart.Chart{
+ Metadata: &chart.Metadata{Name: "ahab"},
+ Values: map[string]interface{}{
+ "global": map[string]interface{}{
+ "nested": map[string]interface{}{"foo": "bar"},
+ "nested2": map[string]interface{}{"l2": "ahab"},
+ },
+ "scope": "ahab",
+ "name": "ahab",
+ "boat": true,
+ "nested": map[string]interface{}{"foo": false, "bar": true},
+ },
+ },
+ ),
+ &chart.Chart{
+ Metadata: &chart.Metadata{Name: "spouter"},
+ Values: map[string]interface{}{
+ "scope": "spouter",
+ "global": map[string]interface{}{
+ "nested2": map[string]interface{}{"l1": "spouter"},
+ },
+ },
+ },
+ )
+
+ vals, err := ReadValues(testCoalesceValuesYaml)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // taking a copy of the values before passing it
+ // to MergeValues as argument, so that we can
+ // use it for asserting later
+ valsCopy := make(Values, len(vals))
+ maps.Copy(valsCopy, vals)
+
+ v, err := MergeValues(c, vals)
+ if err != nil {
+ t.Fatal(err)
+ }
+ j, _ := json.MarshalIndent(v, "", " ")
+ t.Logf("Coalesced Values: %s", string(j))
+
+ tests := []struct {
+ tpl string
+ expect string
+ }{
+ {"{{.top}}", "yup"},
+ {"{{.back}}", ""},
+ {"{{.name}}", "moby"},
+ {"{{.global.name}}", "Ishmael"},
+ {"{{.global.subject}}", "Queequeg"},
+ {"{{.global.harpooner}}", ""},
+ {"{{.pequod.name}}", "pequod"},
+ {"{{.pequod.ahab.name}}", "ahab"},
+ {"{{.pequod.ahab.scope}}", "whale"},
+ {"{{.pequod.ahab.nested.foo}}", "true"},
+ {"{{.pequod.ahab.global.name}}", "Ishmael"},
+ {"{{.pequod.ahab.global.nested.foo}}", "bar"},
+ {"{{.pequod.ahab.global.subject}}", "Queequeg"},
+ {"{{.pequod.ahab.global.harpooner}}", "Tashtego"},
+ {"{{.pequod.global.name}}", "Ishmael"},
+ {"{{.pequod.global.nested.foo}}", ""},
+ {"{{.pequod.global.subject}}", "Queequeg"},
+ {"{{.spouter.global.name}}", "Ishmael"},
+ {"{{.spouter.global.harpooner}}", ""},
+
+ {"{{.global.nested.boat}}", "true"},
+ {"{{.pequod.global.nested.boat}}", "true"},
+ {"{{.spouter.global.nested.boat}}", "true"},
+ {"{{.pequod.global.nested.sail}}", "true"},
+ {"{{.spouter.global.nested.sail}}", ""},
+
+ {"{{.global.nested2.l0}}", "moby"},
+ {"{{.global.nested2.l1}}", ""},
+ {"{{.global.nested2.l2}}", ""},
+ {"{{.pequod.global.nested2.l0}}", "moby"},
+ {"{{.pequod.global.nested2.l1}}", "pequod"},
+ {"{{.pequod.global.nested2.l2}}", ""},
+ {"{{.pequod.ahab.global.nested2.l0}}", "moby"},
+ {"{{.pequod.ahab.global.nested2.l1}}", "pequod"},
+ {"{{.pequod.ahab.global.nested2.l2}}", "ahab"},
+ {"{{.spouter.global.nested2.l0}}", "moby"},
+ {"{{.spouter.global.nested2.l1}}", "spouter"},
+ {"{{.spouter.global.nested2.l2}}", ""},
+ }
+
+ for _, tt := range tests {
+ if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect {
+ t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o)
+ }
+ }
+
+ // nullKeys is different from coalescing. Here the null/nil values are not
+ // removed.
+ nullKeys := []string{"bottom", "right", "left", "front"}
+ for _, nullKey := range nullKeys {
+ if vv, ok := v[nullKey]; !ok {
+ t.Errorf("Expected key %q to be present but it was removed", nullKey)
+ } else if vv != nil {
+ t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv)
+ }
+ }
+
+ if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok {
+ t.Error("Expected nested boat key to be present but it was removed")
+ }
+
+ subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{})
+ if _, ok := subchart["boat"]; !ok {
+ t.Error("Expected subchart boat key to be present but it was removed")
+ }
+
+ if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok {
+ t.Error("Expected subchart nested bar key to be present but it was removed")
+ }
+
+ // CoalesceValues should not mutate the passed arguments
+ is.Equal(valsCopy, vals)
+}
+
+func TestCoalesceTables(t *testing.T) {
+ dst := map[string]interface{}{
+ "name": "Ishmael",
+ "address": map[string]interface{}{
+ "street": "123 Spouter Inn Ct.",
+ "city": "Nantucket",
+ "country": nil,
+ },
+ "details": map[string]interface{}{
+ "friends": []string{"Tashtego"},
+ },
+ "boat": "pequod",
+ "hole": nil,
+ }
+ src := map[string]interface{}{
+ "occupation": "whaler",
+ "address": map[string]interface{}{
+ "state": "MA",
+ "street": "234 Spouter Inn Ct.",
+ "country": "US",
+ },
+ "details": "empty",
+ "boat": map[string]interface{}{
+ "mast": true,
+ },
+ "hole": "black",
+ }
+
+ // What we expect is that anything in dst overrides anything in src, but that
+ // otherwise the values are coalesced.
+ CoalesceTables(dst, src)
+
+ if dst["name"] != "Ishmael" {
+ t.Errorf("Unexpected name: %s", dst["name"])
+ }
+ if dst["occupation"] != "whaler" {
+ t.Errorf("Unexpected occupation: %s", dst["occupation"])
+ }
+
+ addr, ok := dst["address"].(map[string]interface{})
+ if !ok {
+ t.Fatal("Address went away.")
+ }
+
+ if addr["street"].(string) != "123 Spouter Inn Ct." {
+ t.Errorf("Unexpected address: %v", addr["street"])
+ }
+
+ if addr["city"].(string) != "Nantucket" {
+ t.Errorf("Unexpected city: %v", addr["city"])
+ }
+
+ if addr["state"].(string) != "MA" {
+ t.Errorf("Unexpected state: %v", addr["state"])
+ }
+
+ if _, ok = addr["country"]; ok {
+ t.Error("The country is not left out.")
+ }
+
+ if det, ok := dst["details"].(map[string]interface{}); !ok {
+ t.Fatalf("Details is the wrong type: %v", dst["details"])
+ } else if _, ok := det["friends"]; !ok {
+ t.Error("Could not find your friends. Maybe you don't have any. :-(")
+ }
+
+ if dst["boat"].(string) != "pequod" {
+ t.Errorf("Expected boat string, got %v", dst["boat"])
+ }
+
+ if _, ok = dst["hole"]; ok {
+ t.Error("The hole still exists.")
+ }
+
+ dst2 := map[string]interface{}{
+ "name": "Ishmael",
+ "address": map[string]interface{}{
+ "street": "123 Spouter Inn Ct.",
+ "city": "Nantucket",
+ "country": "US",
+ },
+ "details": map[string]interface{}{
+ "friends": []string{"Tashtego"},
+ },
+ "boat": "pequod",
+ "hole": "black",
+ }
+
+ // What we expect is that anything in dst should have all values set,
+ // this happens when the --reuse-values flag is set but the chart has no modifications yet
+ CoalesceTables(dst2, nil)
+
+ if dst2["name"] != "Ishmael" {
+ t.Errorf("Unexpected name: %s", dst2["name"])
+ }
+
+ addr2, ok := dst2["address"].(map[string]interface{})
+ if !ok {
+ t.Fatal("Address went away.")
+ }
+
+ if addr2["street"].(string) != "123 Spouter Inn Ct." {
+ t.Errorf("Unexpected address: %v", addr2["street"])
+ }
+
+ if addr2["city"].(string) != "Nantucket" {
+ t.Errorf("Unexpected city: %v", addr2["city"])
+ }
+
+ if addr2["country"].(string) != "US" {
+ t.Errorf("Unexpected Country: %v", addr2["country"])
+ }
+
+ if det2, ok := dst2["details"].(map[string]interface{}); !ok {
+ t.Fatalf("Details is the wrong type: %v", dst2["details"])
+ } else if _, ok := det2["friends"]; !ok {
+ t.Error("Could not find your friends. Maybe you don't have any. :-(")
+ }
+
+ if dst2["boat"].(string) != "pequod" {
+ t.Errorf("Expected boat string, got %v", dst2["boat"])
+ }
+
+ if dst2["hole"].(string) != "black" {
+ t.Errorf("Expected hole string, got %v", dst2["boat"])
+ }
+}
+
+func TestMergeTables(t *testing.T) {
+ dst := map[string]interface{}{
+ "name": "Ishmael",
+ "address": map[string]interface{}{
+ "street": "123 Spouter Inn Ct.",
+ "city": "Nantucket",
+ "country": nil,
+ },
+ "details": map[string]interface{}{
+ "friends": []string{"Tashtego"},
+ },
+ "boat": "pequod",
+ "hole": nil,
+ }
+ src := map[string]interface{}{
+ "occupation": "whaler",
+ "address": map[string]interface{}{
+ "state": "MA",
+ "street": "234 Spouter Inn Ct.",
+ "country": "US",
+ },
+ "details": "empty",
+ "boat": map[string]interface{}{
+ "mast": true,
+ },
+ "hole": "black",
+ }
+
+ // What we expect is that anything in dst overrides anything in src, but that
+ // otherwise the values are coalesced.
+ MergeTables(dst, src)
+
+ if dst["name"] != "Ishmael" {
+ t.Errorf("Unexpected name: %s", dst["name"])
+ }
+ if dst["occupation"] != "whaler" {
+ t.Errorf("Unexpected occupation: %s", dst["occupation"])
+ }
+
+ addr, ok := dst["address"].(map[string]interface{})
+ if !ok {
+ t.Fatal("Address went away.")
+ }
+
+ if addr["street"].(string) != "123 Spouter Inn Ct." {
+ t.Errorf("Unexpected address: %v", addr["street"])
+ }
+
+ if addr["city"].(string) != "Nantucket" {
+ t.Errorf("Unexpected city: %v", addr["city"])
+ }
+
+ if addr["state"].(string) != "MA" {
+ t.Errorf("Unexpected state: %v", addr["state"])
+ }
+
+ // This is one test that is different from CoalesceTables. Because country
+ // is a nil value and it's not removed it's still present.
+ if _, ok = addr["country"]; !ok {
+ t.Error("The country is left out.")
+ }
+
+ if det, ok := dst["details"].(map[string]interface{}); !ok {
+ t.Fatalf("Details is the wrong type: %v", dst["details"])
+ } else if _, ok := det["friends"]; !ok {
+ t.Error("Could not find your friends. Maybe you don't have any. :-(")
+ }
+
+ if dst["boat"].(string) != "pequod" {
+ t.Errorf("Expected boat string, got %v", dst["boat"])
+ }
+
+ // This is one test that is different from CoalesceTables. Because hole
+ // is a nil value and it's not removed it's still present.
+ if _, ok = dst["hole"]; !ok {
+ t.Error("The hole no longer exists.")
+ }
+
+ dst2 := map[string]interface{}{
+ "name": "Ishmael",
+ "address": map[string]interface{}{
+ "street": "123 Spouter Inn Ct.",
+ "city": "Nantucket",
+ "country": "US",
+ },
+ "details": map[string]interface{}{
+ "friends": []string{"Tashtego"},
+ },
+ "boat": "pequod",
+ "hole": "black",
+ "nilval": nil,
+ }
+
+ // What we expect is that anything in dst should have all values set,
+ // this happens when the --reuse-values flag is set but the chart has no modifications yet
+ MergeTables(dst2, nil)
+
+ if dst2["name"] != "Ishmael" {
+ t.Errorf("Unexpected name: %s", dst2["name"])
+ }
+
+ addr2, ok := dst2["address"].(map[string]interface{})
+ if !ok {
+ t.Fatal("Address went away.")
+ }
+
+ if addr2["street"].(string) != "123 Spouter Inn Ct." {
+ t.Errorf("Unexpected address: %v", addr2["street"])
+ }
+
+ if addr2["city"].(string) != "Nantucket" {
+ t.Errorf("Unexpected city: %v", addr2["city"])
+ }
+
+ if addr2["country"].(string) != "US" {
+ t.Errorf("Unexpected Country: %v", addr2["country"])
+ }
+
+ if det2, ok := dst2["details"].(map[string]interface{}); !ok {
+ t.Fatalf("Details is the wrong type: %v", dst2["details"])
+ } else if _, ok := det2["friends"]; !ok {
+ t.Error("Could not find your friends. Maybe you don't have any. :-(")
+ }
+
+ if dst2["boat"].(string) != "pequod" {
+ t.Errorf("Expected boat string, got %v", dst2["boat"])
+ }
+
+ if dst2["hole"].(string) != "black" {
+ t.Errorf("Expected hole string, got %v", dst2["boat"])
+ }
+
+ if dst2["nilval"] != nil {
+ t.Error("Expected nilvalue to have nil value but it does not")
+ }
+}
+
+func TestCoalesceValuesWarnings(t *testing.T) {
+
+ c := withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "level1"},
+ Values: map[string]interface{}{
+ "name": "moby",
+ },
+ },
+ withDeps(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "level2"},
+ Values: map[string]interface{}{
+ "name": "pequod",
+ },
+ },
+ &chart.Chart{
+ Metadata: &chart.Metadata{Name: "level3"},
+ Values: map[string]interface{}{
+ "name": "ahab",
+ "boat": true,
+ "spear": map[string]interface{}{
+ "tip": true,
+ "sail": map[string]interface{}{
+ "cotton": true,
+ },
+ },
+ },
+ },
+ ),
+ )
+
+ vals := map[string]interface{}{
+ "level2": map[string]interface{}{
+ "level3": map[string]interface{}{
+ "boat": map[string]interface{}{"mast": true},
+ "spear": map[string]interface{}{
+ "tip": map[string]interface{}{
+ "sharp": true,
+ },
+ "sail": true,
+ },
+ },
+ },
+ }
+
+ warnings := make([]string, 0)
+ printf := func(format string, v ...interface{}) {
+ t.Logf(format, v...)
+ warnings = append(warnings, fmt.Sprintf(format, v...))
+ }
+
+ _, err := coalesce(printf, c, vals, "", false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Logf("vals: %v", vals)
+ assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.")
+ assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)")
+ assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])")
+
+}
+
+func TestConcatPrefix(t *testing.T) {
+ assert.Equal(t, "b", concatPrefix("", "b"))
+ assert.Equal(t, "a.b", concatPrefix("a", "b"))
+}
diff --git a/internal/chart/v3/util/compatible.go b/internal/chart/v3/util/compatible.go
new file mode 100644
index 000000000..d384d2d45
--- /dev/null
+++ b/internal/chart/v3/util/compatible.go
@@ -0,0 +1,34 @@
+/*
+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 util
+
+import "github.com/Masterminds/semver/v3"
+
+// IsCompatibleRange compares a version to a constraint.
+// It returns true if the version matches the constraint, and false in all other cases.
+func IsCompatibleRange(constraint, ver string) bool {
+ sv, err := semver.NewVersion(ver)
+ if err != nil {
+ return false
+ }
+
+ c, err := semver.NewConstraint(constraint)
+ if err != nil {
+ return false
+ }
+ return c.Check(sv)
+}
diff --git a/internal/chart/v3/util/compatible_test.go b/internal/chart/v3/util/compatible_test.go
new file mode 100644
index 000000000..e17d33e35
--- /dev/null
+++ b/internal/chart/v3/util/compatible_test.go
@@ -0,0 +1,43 @@
+/*
+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 version represents the current version of the project.
+package util
+
+import "testing"
+
+func TestIsCompatibleRange(t *testing.T) {
+ tests := []struct {
+ constraint string
+ ver string
+ expected bool
+ }{
+ {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true},
+ {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false},
+ {"v2.0.0", "v2.0.0-alpha.4", false},
+ {"v2.0.0-alpha.4", "v2.0.0", false},
+ {"~v2.0.0", "v2.0.1", true},
+ {"v2", "v2.0.0", true},
+ {">2.0.0", "v2.1.1", true},
+ {"v2.1.*", "v2.1.1", true},
+ }
+
+ for _, tt := range tests {
+ if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected {
+ t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver)
+ }
+ }
+}
diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go
new file mode 100644
index 000000000..72fed5955
--- /dev/null
+++ b/internal/chart/v3/util/create.go
@@ -0,0 +1,832 @@
+/*
+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 util
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/chart/v3/loader"
+)
+
+// chartName is a regular expression for testing the supplied name of a chart.
+// This regular expression is probably stricter than it needs to be. We can relax it
+// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be
+// problematic.
+var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
+
+const (
+ // ChartfileName is the default Chart file name.
+ ChartfileName = "Chart.yaml"
+ // ValuesfileName is the default values file name.
+ ValuesfileName = "values.yaml"
+ // SchemafileName is the default values schema file name.
+ SchemafileName = "values.schema.json"
+ // TemplatesDir is the relative directory name for templates.
+ TemplatesDir = "templates"
+ // ChartsDir is the relative directory name for charts dependencies.
+ ChartsDir = "charts"
+ // TemplatesTestsDir is the relative directory name for tests.
+ TemplatesTestsDir = TemplatesDir + sep + "tests"
+ // IgnorefileName is the name of the Helm ignore file.
+ IgnorefileName = ".helmignore"
+ // IngressFileName is the name of the example ingress file.
+ IngressFileName = TemplatesDir + sep + "ingress.yaml"
+ // HTTPRouteFileName is the name of the example HTTPRoute file.
+ HTTPRouteFileName = TemplatesDir + sep + "httproute.yaml"
+ // DeploymentName is the name of the example deployment file.
+ DeploymentName = TemplatesDir + sep + "deployment.yaml"
+ // ServiceName is the name of the example service file.
+ ServiceName = TemplatesDir + sep + "service.yaml"
+ // ServiceAccountName is the name of the example serviceaccount file.
+ ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml"
+ // HorizontalPodAutoscalerName is the name of the example hpa file.
+ HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml"
+ // NotesName is the name of the example NOTES.txt file.
+ NotesName = TemplatesDir + sep + "NOTES.txt"
+ // HelpersName is the name of the example helpers file.
+ HelpersName = TemplatesDir + sep + "_helpers.tpl"
+ // TestConnectionName is the name of the example test file.
+ TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml"
+)
+
+// maxChartNameLength is lower than the limits we know of with certain file systems,
+// and with certain Kubernetes fields.
+const maxChartNameLength = 250
+
+const sep = string(filepath.Separator)
+
+const defaultChartfile = `apiVersion: v3
+name: %s
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.16.0"
+`
+
+const defaultValues = `# Default values for %s.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
+replicaCount: 1
+
+# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
+image:
+ repository: nginx
+ # This sets the pull policy for images.
+ pullPolicy: IfNotPresent
+ # Overrides the image tag whose default is the chart appVersion.
+ tag: ""
+
+# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
+imagePullSecrets: []
+# This is to override the chart name.
+nameOverride: ""
+fullnameOverride: ""
+
+# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
+serviceAccount:
+ # Specifies whether a service account should be created
+ create: true
+ # Automatically mount a ServiceAccount's API credentials?
+ automount: true
+ # Annotations to add to the service account
+ annotations: {}
+ # The name of the service account to use.
+ # If not set and create is true, a name is generated using the fullname template
+ name: ""
+
+# This is for setting Kubernetes Annotations to a Pod.
+# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
+podAnnotations: {}
+# This is for setting Kubernetes Labels to a Pod.
+# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
+podLabels: {}
+
+podSecurityContext: {}
+ # fsGroup: 2000
+
+securityContext: {}
+ # capabilities:
+ # drop:
+ # - ALL
+ # readOnlyRootFilesystem: true
+ # runAsNonRoot: true
+ # runAsUser: 1000
+
+# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
+service:
+ # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
+ type: ClusterIP
+ # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
+ port: 80
+
+# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
+ingress:
+ enabled: false
+ className: ""
+ annotations: {}
+ # kubernetes.io/ingress.class: nginx
+ # kubernetes.io/tls-acme: "true"
+ hosts:
+ - host: chart-example.local
+ paths:
+ - path: /
+ pathType: ImplementationSpecific
+ tls: []
+ # - secretName: chart-example-tls
+ # hosts:
+ # - chart-example.local
+
+# -- Expose the service via gateway-api HTTPRoute
+# Requires Gateway API resources and suitable controller installed within the cluster
+# (see: https://gateway-api.sigs.k8s.io/guides/)
+httpRoute:
+ # HTTPRoute enabled.
+ enabled: false
+ # HTTPRoute annotations.
+ annotations: {}
+ # Which Gateways this Route is attached to.
+ parentRefs:
+ - name: gateway
+ sectionName: http
+ # namespace: default
+ # Hostnames matching HTTP header.
+ hostnames:
+ - chart-example.local
+ # List of rules and filters applied.
+ rules:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /headers
+ # filters:
+ # - type: RequestHeaderModifier
+ # requestHeaderModifier:
+ # set:
+ # - name: My-Overwrite-Header
+ # value: this-is-the-only-value
+ # remove:
+ # - User-Agent
+ # - matches:
+ # - path:
+ # type: PathPrefix
+ # value: /echo
+ # headers:
+ # - name: version
+ # value: v2
+
+resources: {}
+ # We usually recommend not to specify default resources and to leave this as a conscious
+ # choice for the user. This also increases chances charts run on environments with little
+ # resources, such as Minikube. If you do want to specify resources, uncomment the following
+ # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+ # limits:
+ # cpu: 100m
+ # memory: 128Mi
+ # requests:
+ # cpu: 100m
+ # memory: 128Mi
+
+# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
+livenessProbe:
+ httpGet:
+ path: /
+ port: http
+readinessProbe:
+ httpGet:
+ path: /
+ port: http
+
+# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
+autoscaling:
+ enabled: false
+ minReplicas: 1
+ maxReplicas: 100
+ targetCPUUtilizationPercentage: 80
+ # targetMemoryUtilizationPercentage: 80
+
+# Additional volumes on the output Deployment definition.
+volumes: []
+# - name: foo
+# secret:
+# secretName: mysecret
+# optional: false
+
+# Additional volumeMounts on the output Deployment definition.
+volumeMounts: []
+# - name: foo
+# mountPath: "/etc/foo"
+# readOnly: true
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+`
+
+const defaultIgnore = `# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
+`
+
+const defaultIngress = `{{- if .Values.ingress.enabled -}}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ include ".fullname" . }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+ {{- with .Values.ingress.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- with .Values.ingress.className }}
+ ingressClassName: {{ . }}
+ {{- end }}
+ {{- if .Values.ingress.tls }}
+ tls:
+ {{- range .Values.ingress.tls }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- range .Values.ingress.hosts }}
+ - host: {{ .host | quote }}
+ http:
+ paths:
+ {{- range .paths }}
+ - path: {{ .path }}
+ {{- with .pathType }}
+ pathType: {{ . }}
+ {{- end }}
+ backend:
+ service:
+ name: {{ include ".fullname" $ }}
+ port:
+ number: {{ $.Values.service.port }}
+ {{- end }}
+ {{- end }}
+{{- end }}
+`
+
+const defaultHTTPRoute = `{{- if .Values.httpRoute.enabled -}}
+{{- $fullName := include ".fullname" . -}}
+{{- $svcPort := .Values.service.port -}}
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: {{ $fullName }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+ {{- with .Values.httpRoute.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ parentRefs:
+ {{- with .Values.httpRoute.parentRefs }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ {{- with .Values.httpRoute.hostnames }}
+ hostnames:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ rules:
+ {{- range .Values.httpRoute.rules }}
+ {{- with .matches }}
+ - matches:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .filters }}
+ filters:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ backendRefs:
+ - name: {{ $fullName }}
+ port: {{ $svcPort }}
+ weight: 1
+ {{- end }}
+{{- end }}
+`
+
+const defaultDeployment = `apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include ".fullname" . }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+spec:
+ {{- if not .Values.autoscaling.enabled }}
+ replicas: {{ .Values.replicaCount }}
+ {{- end }}
+ selector:
+ matchLabels:
+ {{- include ".selectorLabels" . | nindent 6 }}
+ template:
+ metadata:
+ {{- with .Values.podAnnotations }}
+ annotations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include ".labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include ".serviceAccountName" . }}
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: {{ .Chart.Name }}
+ {{- with .Values.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.service.port }}
+ protocol: TCP
+ {{- with .Values.livenessProbe }}
+ livenessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.readinessProbe }}
+ readinessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.volumeMounts }}
+ volumeMounts:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.volumes }}
+ volumes:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+`
+
+const defaultService = `apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include ".fullname" . }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ {{- include ".selectorLabels" . | nindent 4 }}
+`
+
+const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include ".serviceAccountName" . }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
+`
+
+const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }}
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: {{ include ".fullname" . }}
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: {{ include ".fullname" . }}
+ minReplicas: {{ .Values.autoscaling.minReplicas }}
+ maxReplicas: {{ .Values.autoscaling.maxReplicas }}
+ metrics:
+ {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
+ {{- end }}
+ {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
+ {{- end }}
+{{- end }}
+`
+
+const defaultNotes = `1. Get the application URL by running these commands:
+{{- if .Values.httpRoute.enabled }}
+{{- if .Values.httpRoute.hostnames }}
+ export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }}
+{{- else }}
+ export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}")
+ {{- end }}
+{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }}
+ echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application"
+
+ NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules.
+ The rules can be set for path, method, header and query parameters.
+ You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml'
+{{- end }}
+{{- else if .Values.ingress.enabled }}
+{{- range $host := .Values.ingress.hosts }}
+ {{- range .paths }}
+ http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
+ {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".fullname" . }})
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+ echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+ NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+ You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".fullname" . }}'
+ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+ echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+ export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
+ echo "Visit http://127.0.0.1:8080 to use your application"
+ kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
+{{- end }}
+`
+
+const defaultHelpers = `{{/*
+Expand the name of the chart.
+*/}}
+{{- define ".name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define ".fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define ".chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define ".labels" -}}
+helm.sh/chart: {{ include ".chart" . }}
+{{ include ".selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define ".selectorLabels" -}}
+app.kubernetes.io/name: {{ include ".name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define ".serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include ".fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
+`
+
+const defaultTestConnection = `apiVersion: v1
+kind: Pod
+metadata:
+ name: "{{ include ".fullname" . }}-test-connection"
+ labels:
+ {{- include ".labels" . | nindent 4 }}
+ annotations:
+ "helm.sh/hook": test
+spec:
+ containers:
+ - name: wget
+ image: busybox
+ command: ['wget']
+ args: ['{{ include ".fullname" . }}:{{ .Values.service.port }}']
+ restartPolicy: Never
+`
+
+// Stderr is an io.Writer to which error messages can be written
+//
+// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
+// compatibility.
+var Stderr io.Writer = os.Stderr
+
+// CreateFrom creates a new chart, but scaffolds it from the src chart.
+func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
+ schart, err := loader.Load(src)
+ if err != nil {
+ return fmt.Errorf("could not load %s: %w", src, err)
+ }
+
+ schart.Metadata = chartfile
+
+ var updatedTemplates []*chart.File
+
+ for _, template := range schart.Templates {
+ newData := transform(string(template.Data), schart.Name())
+ updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData})
+ }
+
+ schart.Templates = updatedTemplates
+ b, err := yaml.Marshal(schart.Values)
+ if err != nil {
+ return fmt.Errorf("reading values file: %w", err)
+ }
+
+ var m map[string]interface{}
+ if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil {
+ return fmt.Errorf("transforming values file: %w", err)
+ }
+ schart.Values = m
+
+ // SaveDir looks for the file values.yaml when saving rather than the values
+ // key in order to preserve the comments in the YAML. The name placeholder
+ // needs to be replaced on that file.
+ for _, f := range schart.Raw {
+ if f.Name == ValuesfileName {
+ f.Data = transform(string(f.Data), schart.Name())
+ }
+ }
+
+ return SaveDir(schart, dest)
+}
+
+// Create creates a new chart in a directory.
+//
+// Inside of dir, this will create a directory based on the name of
+// chartfile.Name. It will then write the Chart.yaml into this directory and
+// create the (empty) appropriate directories.
+//
+// The returned string will point to the newly created directory. It will be
+// an absolute path, even if the provided base directory was relative.
+//
+// If dir does not exist, this will return an error.
+// If Chart.yaml or any directories cannot be created, this will return an
+// error. In such a case, this will attempt to clean up by removing the
+// new chart directory.
+func Create(name, dir string) (string, error) {
+
+ // Sanity-check the name of a chart so user doesn't create one that causes problems.
+ if err := validateChartName(name); err != nil {
+ return "", err
+ }
+
+ path, err := filepath.Abs(dir)
+ if err != nil {
+ return path, err
+ }
+
+ if fi, err := os.Stat(path); err != nil {
+ return path, err
+ } else if !fi.IsDir() {
+ return path, fmt.Errorf("no such directory %s", path)
+ }
+
+ cdir := filepath.Join(path, name)
+ if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() {
+ return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir)
+ }
+
+ // Note: If adding a new template below (i.e., to `helm create`) which is disabled by default (similar to hpa and
+ // ingress below); or making an existing template disabled by default, add the enabling condition in
+ // `TestHelmCreateChart_CheckDeprecatedWarnings` in `pkg/lint/lint_test.go` to make it run through deprecation checks
+ // with latest Kubernetes version.
+ files := []struct {
+ path string
+ content []byte
+ }{
+ {
+ // Chart.yaml
+ path: filepath.Join(cdir, ChartfileName),
+ content: []byte(fmt.Sprintf(defaultChartfile, name)),
+ },
+ {
+ // values.yaml
+ path: filepath.Join(cdir, ValuesfileName),
+ content: []byte(fmt.Sprintf(defaultValues, name)),
+ },
+ {
+ // .helmignore
+ path: filepath.Join(cdir, IgnorefileName),
+ content: []byte(defaultIgnore),
+ },
+ {
+ // ingress.yaml
+ path: filepath.Join(cdir, IngressFileName),
+ content: transform(defaultIngress, name),
+ },
+ {
+ // httproute.yaml
+ path: filepath.Join(cdir, HTTPRouteFileName),
+ content: transform(defaultHTTPRoute, name),
+ },
+ {
+ // deployment.yaml
+ path: filepath.Join(cdir, DeploymentName),
+ content: transform(defaultDeployment, name),
+ },
+ {
+ // service.yaml
+ path: filepath.Join(cdir, ServiceName),
+ content: transform(defaultService, name),
+ },
+ {
+ // serviceaccount.yaml
+ path: filepath.Join(cdir, ServiceAccountName),
+ content: transform(defaultServiceAccount, name),
+ },
+ {
+ // hpa.yaml
+ path: filepath.Join(cdir, HorizontalPodAutoscalerName),
+ content: transform(defaultHorizontalPodAutoscaler, name),
+ },
+ {
+ // NOTES.txt
+ path: filepath.Join(cdir, NotesName),
+ content: transform(defaultNotes, name),
+ },
+ {
+ // _helpers.tpl
+ path: filepath.Join(cdir, HelpersName),
+ content: transform(defaultHelpers, name),
+ },
+ {
+ // test-connection.yaml
+ path: filepath.Join(cdir, TestConnectionName),
+ content: transform(defaultTestConnection, name),
+ },
+ }
+
+ for _, file := range files {
+ if _, err := os.Stat(file.path); err == nil {
+ // There is no handle to a preferred output stream here.
+ fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path)
+ }
+ if err := writeFile(file.path, file.content); err != nil {
+ return cdir, err
+ }
+ }
+ // Need to add the ChartsDir explicitly as it does not contain any file OOTB
+ if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil {
+ return cdir, err
+ }
+ return cdir, nil
+}
+
+// transform performs a string replacement of the specified source for
+// a given key with the replacement string
+func transform(src, replacement string) []byte {
+ return []byte(strings.ReplaceAll(src, "", replacement))
+}
+
+func writeFile(name string, content []byte) error {
+ if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
+ return err
+ }
+ return os.WriteFile(name, content, 0644)
+}
+
+func validateChartName(name string) error {
+ if name == "" || len(name) > maxChartNameLength {
+ return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength)
+ }
+ if !chartName.MatchString(name) {
+ return fmt.Errorf("chart name must match the regular expression %q", chartName.String())
+ }
+ return nil
+}
diff --git a/internal/chart/v3/util/create_test.go b/internal/chart/v3/util/create_test.go
new file mode 100644
index 000000000..b3b58cc5a
--- /dev/null
+++ b/internal/chart/v3/util/create_test.go
@@ -0,0 +1,172 @@
+/*
+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 util
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/chart/v3/loader"
+)
+
+func TestCreate(t *testing.T) {
+ tdir := t.TempDir()
+
+ c, err := Create("foo", tdir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ dir := filepath.Join(tdir, "foo")
+
+ mychart, err := loader.LoadDir(c)
+ if err != nil {
+ t.Fatalf("Failed to load newly created chart %q: %s", c, err)
+ }
+
+ if mychart.Name() != "foo" {
+ t.Errorf("Expected name to be 'foo', got %q", mychart.Name())
+ }
+
+ for _, f := range []string{
+ ChartfileName,
+ DeploymentName,
+ HelpersName,
+ IgnorefileName,
+ NotesName,
+ ServiceAccountName,
+ ServiceName,
+ TemplatesDir,
+ TemplatesTestsDir,
+ TestConnectionName,
+ ValuesfileName,
+ } {
+ if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
+ t.Errorf("Expected %s file: %s", f, err)
+ }
+ }
+}
+
+func TestCreateFrom(t *testing.T) {
+ tdir := t.TempDir()
+
+ cf := &chart.Metadata{
+ APIVersion: chart.APIVersionV3,
+ Name: "foo",
+ Version: "0.1.0",
+ }
+ srcdir := "./testdata/frobnitz/charts/mariner"
+
+ if err := CreateFrom(cf, tdir, srcdir); err != nil {
+ t.Fatal(err)
+ }
+
+ dir := filepath.Join(tdir, "foo")
+ c := filepath.Join(tdir, cf.Name)
+ mychart, err := loader.LoadDir(c)
+ if err != nil {
+ t.Fatalf("Failed to load newly created chart %q: %s", c, err)
+ }
+
+ if mychart.Name() != "foo" {
+ t.Errorf("Expected name to be 'foo', got %q", mychart.Name())
+ }
+
+ for _, f := range []string{
+ ChartfileName,
+ ValuesfileName,
+ filepath.Join(TemplatesDir, "placeholder.tpl"),
+ } {
+ if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
+ t.Errorf("Expected %s file: %s", f, err)
+ }
+
+ // Check each file to make sure has been replaced
+ b, err := os.ReadFile(filepath.Join(dir, f))
+ if err != nil {
+ t.Errorf("Unable to read file %s: %s", f, err)
+ }
+ if bytes.Contains(b, []byte("")) {
+ t.Errorf("File %s contains ", f)
+ }
+ }
+}
+
+// TestCreate_Overwrite is a regression test for making sure that files are overwritten.
+func TestCreate_Overwrite(t *testing.T) {
+ tdir := t.TempDir()
+
+ var errlog bytes.Buffer
+
+ if _, err := Create("foo", tdir); err != nil {
+ t.Fatal(err)
+ }
+
+ dir := filepath.Join(tdir, "foo")
+
+ tplname := filepath.Join(dir, "templates/hpa.yaml")
+ writeFile(tplname, []byte("FOO"))
+
+ // Now re-run the create
+ Stderr = &errlog
+ if _, err := Create("foo", tdir); err != nil {
+ t.Fatal(err)
+ }
+
+ data, err := os.ReadFile(tplname)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(data) == "FOO" {
+ t.Fatal("File that should have been modified was not.")
+ }
+
+ if errlog.Len() == 0 {
+ t.Errorf("Expected warnings about overwriting files.")
+ }
+}
+
+func TestValidateChartName(t *testing.T) {
+ for name, shouldPass := range map[string]bool{
+ "": false,
+ "abcdefghijklmnopqrstuvwxyz-_.": true,
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true,
+ "$hello": false,
+ "Hellô": false,
+ "he%%o": false,
+ "he\nllo": false,
+
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "abcdefghijklmnopqrstuvwxyz-_." +
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false,
+ } {
+ if err := validateChartName(name); (err != nil) == shouldPass {
+ t.Errorf("test for %q failed", name)
+ }
+ }
+}
diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go
new file mode 100644
index 000000000..bd5032ce4
--- /dev/null
+++ b/internal/chart/v3/util/dependencies.go
@@ -0,0 +1,366 @@
+/*
+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 util
+
+import (
+ "log/slog"
+ "strings"
+
+ "github.com/mitchellh/copystructure"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// ProcessDependencies checks through this chart's dependencies, processing accordingly.
+func ProcessDependencies(c *chart.Chart, v Values) error {
+ if err := processDependencyEnabled(c, v, ""); err != nil {
+ return err
+ }
+ return processDependencyImportValues(c, true)
+}
+
+// processDependencyConditions disables charts based on condition path value in values
+func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) {
+ if reqs == nil {
+ return
+ }
+ for _, r := range reqs {
+ for c := range strings.SplitSeq(strings.TrimSpace(r.Condition), ",") {
+ if len(c) > 0 {
+ // retrieve value
+ vv, err := cvals.PathValue(cpath + c)
+ if err == nil {
+ // if not bool, warn
+ if bv, ok := vv.(bool); ok {
+ r.Enabled = bv
+ break
+ }
+ slog.Warn("returned non-bool value", "path", c, "chart", r.Name)
+ } else if _, ok := err.(ErrNoValue); !ok {
+ // this is a real error
+ slog.Warn("the method PathValue returned error", slog.Any("error", err))
+ }
+ }
+ }
+ }
+}
+
+// processDependencyTags disables charts based on tags in values
+func processDependencyTags(reqs []*chart.Dependency, cvals Values) {
+ if reqs == nil {
+ return
+ }
+ vt, err := cvals.Table("tags")
+ if err != nil {
+ return
+ }
+ for _, r := range reqs {
+ var hasTrue, hasFalse bool
+ for _, k := range r.Tags {
+ if b, ok := vt[k]; ok {
+ // if not bool, warn
+ if bv, ok := b.(bool); ok {
+ if bv {
+ hasTrue = true
+ } else {
+ hasFalse = true
+ }
+ } else {
+ slog.Warn("returned non-bool value", "tag", k, "chart", r.Name)
+ }
+ }
+ }
+ if !hasTrue && hasFalse {
+ r.Enabled = false
+ } else if hasTrue || !hasTrue && !hasFalse {
+ r.Enabled = true
+ }
+ }
+}
+
+// getAliasDependency finds the chart for an alias dependency and copies parts that will be modified
+func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart {
+ for _, c := range charts {
+ if c == nil {
+ continue
+ }
+ if c.Name() != dep.Name {
+ continue
+ }
+ if !IsCompatibleRange(dep.Version, c.Metadata.Version) {
+ continue
+ }
+
+ out := *c
+ out.Metadata = copyMetadata(c.Metadata)
+
+ // empty dependencies and shallow copy all dependencies, otherwise parent info may be corrupted if
+ // there is more than one dependency aliasing this chart
+ out.SetDependencies()
+ for _, dependency := range c.Dependencies() {
+ cpy := *dependency
+ out.AddDependency(&cpy)
+ }
+
+ if dep.Alias != "" {
+ out.Metadata.Name = dep.Alias
+ }
+ return &out
+ }
+ return nil
+}
+
+func copyMetadata(metadata *chart.Metadata) *chart.Metadata {
+ md := *metadata
+
+ if md.Dependencies != nil {
+ dependencies := make([]*chart.Dependency, len(md.Dependencies))
+ for i := range md.Dependencies {
+ dependency := *md.Dependencies[i]
+ dependencies[i] = &dependency
+ }
+ md.Dependencies = dependencies
+ }
+ return &md
+}
+
+// processDependencyEnabled removes disabled charts from dependencies
+func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error {
+ if c.Metadata.Dependencies == nil {
+ return nil
+ }
+
+ var chartDependencies []*chart.Chart
+ // If any dependency is not a part of Chart.yaml
+ // then this should be added to chartDependencies.
+ // However, if the dependency is already specified in Chart.yaml
+ // we should not add it, as it would be processed from Chart.yaml anyway.
+
+Loop:
+ for _, existing := range c.Dependencies() {
+ for _, req := range c.Metadata.Dependencies {
+ if existing.Name() == req.Name && IsCompatibleRange(req.Version, existing.Metadata.Version) {
+ continue Loop
+ }
+ }
+ chartDependencies = append(chartDependencies, existing)
+ }
+
+ for _, req := range c.Metadata.Dependencies {
+ if req == nil {
+ continue
+ }
+ if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil {
+ chartDependencies = append(chartDependencies, chartDependency)
+ }
+ if req.Alias != "" {
+ req.Name = req.Alias
+ }
+ }
+ c.SetDependencies(chartDependencies...)
+
+ // set all to true
+ for _, lr := range c.Metadata.Dependencies {
+ lr.Enabled = true
+ }
+ cvals, err := CoalesceValues(c, v)
+ if err != nil {
+ return err
+ }
+ // flag dependencies as enabled/disabled
+ processDependencyTags(c.Metadata.Dependencies, cvals)
+ processDependencyConditions(c.Metadata.Dependencies, cvals, path)
+ // make a map of charts to remove
+ rm := map[string]struct{}{}
+ for _, r := range c.Metadata.Dependencies {
+ if !r.Enabled {
+ // remove disabled chart
+ rm[r.Name] = struct{}{}
+ }
+ }
+ // don't keep disabled charts in new slice
+ cd := []*chart.Chart{}
+ copy(cd, c.Dependencies()[:0])
+ for _, n := range c.Dependencies() {
+ if _, ok := rm[n.Metadata.Name]; !ok {
+ cd = append(cd, n)
+ }
+ }
+ // don't keep disabled charts in metadata
+ cdMetadata := []*chart.Dependency{}
+ copy(cdMetadata, c.Metadata.Dependencies[:0])
+ for _, n := range c.Metadata.Dependencies {
+ if _, ok := rm[n.Name]; !ok {
+ cdMetadata = append(cdMetadata, n)
+ }
+ }
+
+ // recursively call self to process sub dependencies
+ for _, t := range cd {
+ subpath := path + t.Metadata.Name + "."
+ if err := processDependencyEnabled(t, cvals, subpath); err != nil {
+ return err
+ }
+ }
+ // set the correct dependencies in metadata
+ c.Metadata.Dependencies = nil
+ c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...)
+ c.SetDependencies(cd...)
+
+ return nil
+}
+
+// pathToMap creates a nested map given a YAML path in dot notation.
+func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
+ if path == "." {
+ return data
+ }
+ return set(parsePath(path), data)
+}
+
+func set(path []string, data map[string]interface{}) map[string]interface{} {
+ if len(path) == 0 {
+ return nil
+ }
+ cur := data
+ for i := len(path) - 1; i >= 0; i-- {
+ cur = map[string]interface{}{path[i]: cur}
+ }
+ return cur
+}
+
+// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
+func processImportValues(c *chart.Chart, merge bool) error {
+ if c.Metadata.Dependencies == nil {
+ return nil
+ }
+ // combine chart values and empty config to get Values
+ var cvals Values
+ var err error
+ if merge {
+ cvals, err = MergeValues(c, nil)
+ } else {
+ cvals, err = CoalesceValues(c, nil)
+ }
+ if err != nil {
+ return err
+ }
+ b := make(map[string]interface{})
+ // import values from each dependency if specified in import-values
+ for _, r := range c.Metadata.Dependencies {
+ var outiv []interface{}
+ for _, riv := range r.ImportValues {
+ switch iv := riv.(type) {
+ case map[string]interface{}:
+ child := iv["child"].(string)
+ parent := iv["parent"].(string)
+
+ outiv = append(outiv, map[string]string{
+ "child": child,
+ "parent": parent,
+ })
+
+ // get child table
+ vv, err := cvals.Table(r.Name + "." + child)
+ if err != nil {
+ slog.Warn("ImportValues missing table from chart", "chart", r.Name, slog.Any("error", err))
+ continue
+ }
+ // create value map from child to be merged into parent
+ if merge {
+ b = MergeTables(b, pathToMap(parent, vv.AsMap()))
+ } else {
+ b = CoalesceTables(b, pathToMap(parent, vv.AsMap()))
+ }
+ case string:
+ child := "exports." + iv
+ outiv = append(outiv, map[string]string{
+ "child": child,
+ "parent": ".",
+ })
+ vm, err := cvals.Table(r.Name + "." + child)
+ if err != nil {
+ slog.Warn("ImportValues missing table", slog.Any("error", err))
+ continue
+ }
+ if merge {
+ b = MergeTables(b, vm.AsMap())
+ } else {
+ b = CoalesceTables(b, vm.AsMap())
+ }
+ }
+ }
+ r.ImportValues = outiv
+ }
+
+ // Imported values from a child to a parent chart have a lower priority than
+ // the parents values. This enables parent charts to import a large section
+ // from a child and then override select parts. This is why b is merged into
+ // cvals in the code below and not the other way around.
+ if merge {
+ // deep copying the cvals as there are cases where pointers can end
+ // up in the cvals when they are copied onto b in ways that break things.
+ cvals = deepCopyMap(cvals)
+ c.Values = MergeTables(cvals, b)
+ } else {
+ // Trimming the nil values from cvals is needed for backwards compatibility.
+ // Previously, the b value had been populated with cvals along with some
+ // overrides. This caused the coalescing functionality to remove the
+ // nil/null values. This trimming is for backwards compat.
+ cvals = trimNilValues(cvals)
+ c.Values = CoalesceTables(cvals, b)
+ }
+
+ return nil
+}
+
+func deepCopyMap(vals map[string]interface{}) map[string]interface{} {
+ valsCopy, err := copystructure.Copy(vals)
+ if err != nil {
+ return vals
+ }
+ return valsCopy.(map[string]interface{})
+}
+
+func trimNilValues(vals map[string]interface{}) map[string]interface{} {
+ valsCopy, err := copystructure.Copy(vals)
+ if err != nil {
+ return vals
+ }
+ valsCopyMap := valsCopy.(map[string]interface{})
+ for key, val := range valsCopyMap {
+ if val == nil {
+ // Iterate over the values and remove nil keys
+ delete(valsCopyMap, key)
+ } else if istable(val) {
+ // Recursively call into ourselves to remove keys from inner tables
+ valsCopyMap[key] = trimNilValues(val.(map[string]interface{}))
+ }
+ }
+
+ return valsCopyMap
+}
+
+// processDependencyImportValues imports specified chart values from child to parent.
+func processDependencyImportValues(c *chart.Chart, merge bool) error {
+ for _, d := range c.Dependencies() {
+ // recurse
+ if err := processDependencyImportValues(d, merge); err != nil {
+ return err
+ }
+ }
+ return processImportValues(c, merge)
+}
diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go
new file mode 100644
index 000000000..55839fe65
--- /dev/null
+++ b/internal/chart/v3/util/dependencies_test.go
@@ -0,0 +1,569 @@
+/*
+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 util
+
+import (
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "testing"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/chart/v3/loader"
+)
+
+func loadChart(t *testing.T, path string) *chart.Chart {
+ t.Helper()
+ c, err := loader.Load(path)
+ if err != nil {
+ t.Fatalf("failed to load testdata: %s", err)
+ }
+ return c
+}
+
+func TestLoadDependency(t *testing.T) {
+ tests := []*chart.Dependency{
+ {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
+ {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
+ }
+
+ check := func(deps []*chart.Dependency) {
+ if len(deps) != 2 {
+ t.Errorf("expected 2 dependencies, got %d", len(deps))
+ }
+ for i, tt := range tests {
+ if deps[i].Name != tt.Name {
+ t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name)
+ }
+ if deps[i].Version != tt.Version {
+ t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version)
+ }
+ if deps[i].Repository != tt.Repository {
+ t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository)
+ }
+ }
+ }
+ c := loadChart(t, "testdata/frobnitz")
+ check(c.Metadata.Dependencies)
+ check(c.Lock.Dependencies)
+}
+
+func TestDependencyEnabled(t *testing.T) {
+ type M = map[string]interface{}
+ tests := []struct {
+ name string
+ v M
+ e []string // expected charts including duplicates in alphanumeric order
+ }{{
+ "tags with no effect",
+ M{"tags": M{"nothinguseful": false}},
+ []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"},
+ }, {
+ "tags disabling a group",
+ M{"tags": M{"front-end": false}},
+ []string{"parentchart"},
+ }, {
+ "tags disabling a group and enabling a different group",
+ M{"tags": M{"front-end": false, "back-end": true}},
+ []string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"},
+ }, {
+ "tags disabling only children, children still enabled since tag front-end=true in values.yaml",
+ M{"tags": M{"subcharta": false, "subchartb": false}},
+ []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"},
+ }, {
+ "tags disabling all parents/children with additional tag re-enabling a parent",
+ M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}},
+ []string{"parentchart", "parentchart.subchart1"},
+ }, {
+ "conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml",
+ M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}},
+ []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"},
+ }, {
+ "conditions disabling the parent charts, effectively disabling children",
+ M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}},
+ []string{"parentchart"},
+ }, {
+ "conditions a child using the second condition path of child's condition",
+ M{"subchart1": M{"subcharta": M{"enabled": false}}},
+ []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"},
+ }, {
+ "tags enabling a parent/child group with condition disabling one child",
+ M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}},
+ []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"},
+ }, {
+ "tags will not enable a child if parent is explicitly disabled with condition",
+ M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}},
+ []string{"parentchart"},
+ }, {
+ "subcharts with alias also respect conditions",
+ M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}},
+ []string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"},
+ }}
+
+ for _, tc := range tests {
+ c := loadChart(t, "testdata/subpop")
+ t.Run(tc.name, func(t *testing.T) {
+ if err := processDependencyEnabled(c, tc.v, ""); err != nil {
+ t.Fatalf("error processing enabled dependencies %v", err)
+ }
+
+ names := extractChartNames(c)
+ if len(names) != len(tc.e) {
+ t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e))
+ }
+ for i := range names {
+ if names[i] != tc.e[i] {
+ t.Fatalf("slice values do not match got %v, expected %v", names, tc.e)
+ }
+ }
+ })
+ }
+}
+
+// extractChartNames recursively searches chart dependencies returning all charts found
+func extractChartNames(c *chart.Chart) []string {
+ var out []string
+ var fn func(c *chart.Chart)
+ fn = func(c *chart.Chart) {
+ out = append(out, c.ChartPath())
+ for _, d := range c.Dependencies() {
+ fn(d)
+ }
+ }
+ fn(c)
+ sort.Strings(out)
+ return out
+}
+
+func TestProcessDependencyImportValues(t *testing.T) {
+ c := loadChart(t, "testdata/subpop")
+
+ e := make(map[string]string)
+
+ e["imported-chart1.SC1bool"] = "true"
+ e["imported-chart1.SC1float"] = "3.14"
+ e["imported-chart1.SC1int"] = "100"
+ e["imported-chart1.SC1string"] = "dollywood"
+ e["imported-chart1.SC1extra1"] = "11"
+ e["imported-chart1.SPextra1"] = "helm rocks"
+ e["imported-chart1.SC1extra1"] = "11"
+
+ e["imported-chartA.SCAbool"] = "false"
+ e["imported-chartA.SCAfloat"] = "3.1"
+ e["imported-chartA.SCAint"] = "55"
+ e["imported-chartA.SCAstring"] = "jabba"
+ e["imported-chartA.SPextra3"] = "1.337"
+ e["imported-chartA.SC1extra2"] = "1.337"
+ e["imported-chartA.SCAnested1.SCAnested2"] = "true"
+
+ e["imported-chartA-B.SCAbool"] = "false"
+ e["imported-chartA-B.SCAfloat"] = "3.1"
+ e["imported-chartA-B.SCAint"] = "55"
+ e["imported-chartA-B.SCAstring"] = "jabba"
+
+ e["imported-chartA-B.SCBbool"] = "true"
+ e["imported-chartA-B.SCBfloat"] = "7.77"
+ e["imported-chartA-B.SCBint"] = "33"
+ e["imported-chartA-B.SCBstring"] = "boba"
+ e["imported-chartA-B.SPextra5"] = "k8s"
+ e["imported-chartA-B.SC1extra5"] = "tiller"
+
+ // These values are imported from the child chart to the parent. Parent
+ // values take precedence over imported values. This enables importing a
+ // large section from a child chart and overriding a selection from it.
+ e["overridden-chart1.SC1bool"] = "false"
+ e["overridden-chart1.SC1float"] = "3.141592"
+ e["overridden-chart1.SC1int"] = "99"
+ e["overridden-chart1.SC1string"] = "pollywog"
+ e["overridden-chart1.SPextra2"] = "42"
+
+ e["overridden-chartA.SCAbool"] = "true"
+ e["overridden-chartA.SCAfloat"] = "41.3"
+ e["overridden-chartA.SCAint"] = "808"
+ e["overridden-chartA.SCAstring"] = "jabberwocky"
+ e["overridden-chartA.SPextra4"] = "true"
+
+ // These values are imported from the child chart to the parent. Parent
+ // values take precedence over imported values. This enables importing a
+ // large section from a child chart and overriding a selection from it.
+ e["overridden-chartA-B.SCAbool"] = "true"
+ e["overridden-chartA-B.SCAfloat"] = "41.3"
+ e["overridden-chartA-B.SCAint"] = "808"
+ e["overridden-chartA-B.SCAstring"] = "jabberwocky"
+ e["overridden-chartA-B.SCBbool"] = "false"
+ e["overridden-chartA-B.SCBfloat"] = "1.99"
+ e["overridden-chartA-B.SCBint"] = "77"
+ e["overridden-chartA-B.SCBstring"] = "jango"
+ e["overridden-chartA-B.SPextra6"] = "111"
+ e["overridden-chartA-B.SCAextra1"] = "23"
+ e["overridden-chartA-B.SCBextra1"] = "13"
+ e["overridden-chartA-B.SC1extra6"] = "77"
+
+ // `exports` style
+ e["SCBexported1B"] = "1965"
+ e["SC1extra7"] = "true"
+ e["SCBexported2A"] = "blaster"
+ e["global.SC1exported2.all.SC1exported3"] = "SC1expstr"
+
+ if err := processDependencyImportValues(c, false); err != nil {
+ t.Fatalf("processing import values dependencies %v", err)
+ }
+ cc := Values(c.Values)
+ for kk, vv := range e {
+ pv, err := cc.PathValue(kk)
+ if err != nil {
+ t.Fatalf("retrieving import values table %v %v", kk, err)
+ }
+
+ switch pv := pv.(type) {
+ case float64:
+ if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
+ t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk)
+ }
+ case bool:
+ if b := strconv.FormatBool(pv); b != vv {
+ t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk)
+ }
+ default:
+ if pv != vv {
+ t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk)
+ }
+ }
+ }
+
+ // Since this was processed with coalescing there should be no null values.
+ // Here we verify that.
+ _, err := cc.PathValue("ensurenull")
+ if err == nil {
+ t.Error("expect nil value not found but found it")
+ }
+ switch xerr := err.(type) {
+ case ErrNoValue:
+ // We found what we expected
+ default:
+ t.Errorf("expected an ErrNoValue but got %q instead", xerr)
+ }
+
+ c = loadChart(t, "testdata/subpop")
+ if err := processDependencyImportValues(c, true); err != nil {
+ t.Fatalf("processing import values dependencies %v", err)
+ }
+ cc = Values(c.Values)
+ val, err := cc.PathValue("ensurenull")
+ if err != nil {
+ t.Error("expect value but ensurenull was not found")
+ }
+ if val != nil {
+ t.Errorf("expect nil value but got %q instead", val)
+ }
+}
+
+func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) {
+ c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies")
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+ if err := processDependencyImportValues(c, true); err != nil {
+ t.Fatalf("processing import values dependencies %v", err)
+ }
+ e := make(map[string]string)
+
+ e["foo-defaults.defaultValue"] = "42"
+ e["bar-defaults.defaultValue"] = "42"
+
+ e["foo.defaults.defaultValue"] = "42"
+ e["bar.defaults.defaultValue"] = "42"
+
+ e["foo.grandchild.defaults.defaultValue"] = "42"
+ e["bar.grandchild.defaults.defaultValue"] = "42"
+
+ cValues := Values(c.Values)
+ for kk, vv := range e {
+ pv, err := cValues.PathValue(kk)
+ if err != nil {
+ t.Fatalf("retrieving import values table %v %v", kk, err)
+ }
+ if pv != vv {
+ t.Errorf("failed to match imported value %v with expected %v", pv, vv)
+ }
+ }
+}
+
+func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) {
+ c := loadChart(t, "testdata/three-level-dependent-chart/umbrella")
+
+ e := make(map[string]string)
+
+ // The order of precedence should be:
+ // 1. User specified values (e.g CLI)
+ // 2. Parent chart values
+ // 3. Imported values
+ // 4. Sub-chart values
+ // The 4 app charts here deal with things differently:
+ // - app1 has a port value set in the umbrella chart. It does not import any
+ // values so the value from the umbrella chart should be used.
+ // - app2 has a value in the app chart and imports from the library. The
+ // app chart value should take precedence.
+ // - app3 has no value in the app chart and imports the value from the library
+ // chart. The library chart value should be used.
+ // - app4 has a value in the app chart and does not import the value from the
+ // library chart. The app charts value should be used.
+ e["app1.service.port"] = "3456"
+ e["app2.service.port"] = "8080"
+ e["app3.service.port"] = "9090"
+ e["app4.service.port"] = "1234"
+ if err := processDependencyImportValues(c, true); err != nil {
+ t.Fatalf("processing import values dependencies %v", err)
+ }
+ cc := Values(c.Values)
+ for kk, vv := range e {
+ pv, err := cc.PathValue(kk)
+ if err != nil {
+ t.Fatalf("retrieving import values table %v %v", kk, err)
+ }
+
+ switch pv := pv.(type) {
+ case float64:
+ if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv {
+ t.Errorf("failed to match imported float value %v with expected %v", s, vv)
+ }
+ default:
+ if pv != vv {
+ t.Errorf("failed to match imported string value %q with expected %q", pv, vv)
+ }
+ }
+ }
+}
+
+func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) {
+ c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart")
+ nameOverride := "parent-chart-prod"
+
+ if err := processDependencyImportValues(c, true); err != nil {
+ t.Fatalf("processing import values dependencies %v", err)
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 1 {
+ t.Fatal("expected no changes in dependencies")
+ }
+
+ if len(c.Metadata.Dependencies) != 1 {
+ t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
+ }
+
+ prodDependencyValues := c.Dependencies()[0].Values
+ if prodDependencyValues["nameOverride"] != nameOverride {
+ t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"])
+ }
+}
+
+func TestGetAliasDependency(t *testing.T) {
+ c := loadChart(t, "testdata/frobnitz")
+ req := c.Metadata.Dependencies
+
+ if len(req) == 0 {
+ t.Fatalf("there are no dependencies to test")
+ }
+
+ // Success case
+ aliasChart := getAliasDependency(c.Dependencies(), req[0])
+ if aliasChart == nil {
+ t.Fatalf("failed to get dependency chart for alias %s", req[0].Name)
+ }
+ if req[0].Alias != "" {
+ if aliasChart.Name() != req[0].Alias {
+ t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name())
+ }
+ } else if aliasChart.Name() != req[0].Name {
+ t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name())
+ }
+
+ if req[0].Version != "" {
+ if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) {
+ t.Fatalf("dependency chart version is not in the compatible range")
+ }
+ }
+
+ // Failure case
+ req[0].Name = "something-else"
+ if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil {
+ t.Fatalf("expected no chart but got %s", aliasChart.Name())
+ }
+
+ req[0].Version = "something else which is not in the compatible range"
+ if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) {
+ t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ")
+ }
+}
+
+func TestDependentChartAliases(t *testing.T) {
+ c := loadChart(t, "testdata/dependent-chart-alias")
+ req := c.Metadata.Dependencies
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 3 {
+ t.Fatal("expected alias dependencies to be added")
+ }
+
+ if len(c.Dependencies()) != len(c.Metadata.Dependencies) {
+ t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
+ }
+
+ aliasChart := getAliasDependency(c.Dependencies(), req[2])
+
+ if aliasChart == nil {
+ t.Fatalf("failed to get dependency chart for alias %s", req[2].Name)
+ }
+ if aliasChart.Parent() != c {
+ t.Fatalf("dependency chart has wrong parent, expected %s but got %s", c.Name(), aliasChart.Parent().Name())
+ }
+ if req[2].Alias != "" {
+ if aliasChart.Name() != req[2].Alias {
+ t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name())
+ }
+ } else if aliasChart.Name() != req[2].Name {
+ t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name())
+ }
+
+ req[2].Name = "dummy-name"
+ if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil {
+ t.Fatalf("expected no chart but got %s", aliasChart.Name())
+ }
+
+}
+
+func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) {
+ c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml")
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatal("expected no changes in dependencies")
+ }
+}
+
+func TestDependentChartWithSubChartsHelmignore(t *testing.T) {
+ // FIXME what does this test?
+ loadChart(t, "testdata/dependent-chart-helmignore")
+}
+
+func TestDependentChartsWithSubChartsSymlink(t *testing.T) {
+ joonix := filepath.Join("testdata", "joonix")
+ if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz"))
+ c := loadChart(t, joonix)
+
+ if c.Name() != "joonix" {
+ t.Fatalf("unexpected chart name: %s", c.Name())
+ }
+ if n := len(c.Dependencies()); n != 1 {
+ t.Fatalf("expected 1 dependency for this chart, but got %d", n)
+ }
+}
+
+func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) {
+ c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml")
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatal("expected no changes in dependencies")
+ }
+
+ if len(c.Dependencies()) != len(c.Metadata.Dependencies) {
+ t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies()))
+ }
+}
+
+func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) {
+ c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml")
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatal("expected no changes in dependencies")
+ }
+
+ if len(c.Metadata.Dependencies) != 1 {
+ t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies))
+ }
+}
+
+func validateDependencyTree(t *testing.T, c *chart.Chart) {
+ t.Helper()
+ for _, dependency := range c.Dependencies() {
+ if dependency.Parent() != c {
+ if dependency.Parent() != c {
+ t.Fatalf("dependency chart %s has wrong parent, expected %s but got %s", dependency.Name(), c.Name(), dependency.Parent().Name())
+ }
+ }
+ // recurse entire tree
+ validateDependencyTree(t, dependency)
+ }
+}
+
+func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *testing.T) {
+ c := loadChart(t, "testdata/chart-with-dependency-aliased-twice")
+
+ if len(c.Dependencies()) != 1 {
+ t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies()))
+ }
+
+ if err := processDependencyEnabled(c, c.Values, ""); err != nil {
+ t.Fatalf("expected no errors but got %q", err)
+ }
+
+ if len(c.Dependencies()) != 2 {
+ t.Fatal("expected two dependencies after processing aliases")
+ }
+ validateDependencyTree(t, c)
+}
diff --git a/internal/chart/v3/util/doc.go b/internal/chart/v3/util/doc.go
new file mode 100644
index 000000000..002d5babc
--- /dev/null
+++ b/internal/chart/v3/util/doc.go
@@ -0,0 +1,45 @@
+/*
+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 util contains tools for working with charts.
+
+Charts are described in the chart package (pkg/chart).
+This package provides utilities for serializing and deserializing charts.
+
+A chart can be represented on the file system in one of two ways:
+
+ - As a directory that contains a Chart.yaml file and other chart things.
+ - As a tarred gzipped file containing a directory that then contains a
+ Chart.yaml file.
+
+This package provides utilities for working with those file formats.
+
+The preferred way of loading a chart is using 'loader.Load`:
+
+ chart, err := loader.Load(filename)
+
+This will attempt to discover whether the file at 'filename' is a directory or
+a chart archive. It will then load accordingly.
+
+For accepting raw compressed tar file data from an io.Reader, the
+'loader.LoadArchive()' will read in the data, uncompress it, and unpack it
+into a Chart.
+
+When creating charts in memory, use the 'helm.sh/helm/pkg/chart'
+package directly.
+*/
+package util // import chartutil "helm.sh/helm/v4/internal/chart/v3/util"
diff --git a/internal/chart/v3/util/errors.go b/internal/chart/v3/util/errors.go
new file mode 100644
index 000000000..a175b9758
--- /dev/null
+++ b/internal/chart/v3/util/errors.go
@@ -0,0 +1,43 @@
+/*
+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 util
+
+import (
+ "fmt"
+)
+
+// ErrNoTable indicates that a chart does not have a matching table.
+type ErrNoTable struct {
+ Key string
+}
+
+func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) }
+
+// ErrNoValue indicates that Values does not contain a key with a value
+type ErrNoValue struct {
+ Key string
+}
+
+func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) }
+
+type ErrInvalidChartName struct {
+ Name string
+}
+
+func (e ErrInvalidChartName) Error() string {
+ return fmt.Sprintf("%q is not a valid chart name", e.Name)
+}
diff --git a/internal/chart/v3/util/errors_test.go b/internal/chart/v3/util/errors_test.go
new file mode 100644
index 000000000..b8ae86384
--- /dev/null
+++ b/internal/chart/v3/util/errors_test.go
@@ -0,0 +1,37 @@
+/*
+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 util
+
+import (
+ "testing"
+)
+
+func TestErrorNoTableDoesNotPanic(t *testing.T) {
+ x := "empty"
+
+ y := ErrNoTable{x}
+
+ t.Logf("error is: %s", y)
+}
+
+func TestErrorNoValueDoesNotPanic(t *testing.T) {
+ x := "empty"
+
+ y := ErrNoValue{x}
+
+ t.Logf("error is: %s", y)
+}
diff --git a/internal/chart/v3/util/expand.go b/internal/chart/v3/util/expand.go
new file mode 100644
index 000000000..6cbbeabf2
--- /dev/null
+++ b/internal/chart/v3/util/expand.go
@@ -0,0 +1,94 @@
+/*
+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 util
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ securejoin "github.com/cyphar/filepath-securejoin"
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/chart/v3/loader"
+)
+
+// Expand uncompresses and extracts a chart into the specified directory.
+func Expand(dir string, r io.Reader) error {
+ files, err := loader.LoadArchiveFiles(r)
+ if err != nil {
+ return err
+ }
+
+ // Get the name of the chart
+ var chartName string
+ for _, file := range files {
+ if file.Name == "Chart.yaml" {
+ ch := &chart.Metadata{}
+ if err := yaml.Unmarshal(file.Data, ch); err != nil {
+ return fmt.Errorf("cannot load Chart.yaml: %w", err)
+ }
+ chartName = ch.Name
+ }
+ }
+ if chartName == "" {
+ return errors.New("chart name not specified")
+ }
+
+ // Find the base directory
+ // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up
+ // being wrong or returning an error. This was introduced in v0.4.0.
+ dir = filepath.Clean(dir)
+ chartdir, err := securejoin.SecureJoin(dir, chartName)
+ if err != nil {
+ return err
+ }
+
+ // Copy all files verbatim. We don't parse these files because parsing can remove
+ // comments.
+ for _, file := range files {
+ outpath, err := securejoin.SecureJoin(chartdir, file.Name)
+ if err != nil {
+ return err
+ }
+
+ // Make sure the necessary subdirs get created.
+ basedir := filepath.Dir(outpath)
+ if err := os.MkdirAll(basedir, 0755); err != nil {
+ return err
+ }
+
+ if err := os.WriteFile(outpath, file.Data, 0644); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// ExpandFile expands the src file into the dest directory.
+func ExpandFile(dest, src string) error {
+ h, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer h.Close()
+ return Expand(dest, h)
+}
diff --git a/internal/chart/v3/util/expand_test.go b/internal/chart/v3/util/expand_test.go
new file mode 100644
index 000000000..280995f7e
--- /dev/null
+++ b/internal/chart/v3/util/expand_test.go
@@ -0,0 +1,124 @@
+/*
+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 util
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestExpand(t *testing.T) {
+ dest := t.TempDir()
+
+ reader, err := os.Open("testdata/frobnitz-1.2.3.tgz")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Expand(dest, reader); err != nil {
+ t.Fatal(err)
+ }
+
+ expectedChartPath := filepath.Join(dest, "frobnitz")
+ fi, err := os.Stat(expectedChartPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !fi.IsDir() {
+ t.Fatalf("expected a chart directory at %s", expectedChartPath)
+ }
+
+ dir, err := os.Open(expectedChartPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fis, err := dir.Readdir(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectLen := 11
+ if len(fis) != expectLen {
+ t.Errorf("Expected %d files, but got %d", expectLen, len(fis))
+ }
+
+ for _, fi := range fis {
+ expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // os.Stat can return different values for directories, based on the OS
+ // for Linux, for example, os.Stat always returns the size of the directory
+ // (value-4096) regardless of the size of the contents of the directory
+ mode := expect.Mode()
+ if !mode.IsDir() {
+ if fi.Size() != expect.Size() {
+ t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
+ }
+ }
+ }
+}
+
+func TestExpandFile(t *testing.T) {
+ dest := t.TempDir()
+
+ if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil {
+ t.Fatal(err)
+ }
+
+ expectedChartPath := filepath.Join(dest, "frobnitz")
+ fi, err := os.Stat(expectedChartPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !fi.IsDir() {
+ t.Fatalf("expected a chart directory at %s", expectedChartPath)
+ }
+
+ dir, err := os.Open(expectedChartPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ fis, err := dir.Readdir(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expectLen := 11
+ if len(fis) != expectLen {
+ t.Errorf("Expected %d files, but got %d", expectLen, len(fis))
+ }
+
+ for _, fi := range fis {
+ expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name()))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // os.Stat can return different values for directories, based on the OS
+ // for Linux, for example, os.Stat always returns the size of the directory
+ // (value-4096) regardless of the size of the contents of the directory
+ mode := expect.Mode()
+ if !mode.IsDir() {
+ if fi.Size() != expect.Size() {
+ t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size())
+ }
+ }
+ }
+}
diff --git a/internal/chart/v3/util/jsonschema.go b/internal/chart/v3/util/jsonschema.go
new file mode 100644
index 000000000..9fe35904e
--- /dev/null
+++ b/internal/chart/v3/util/jsonschema.go
@@ -0,0 +1,113 @@
+/*
+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 util
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "log/slog"
+ "strings"
+
+ "github.com/santhosh-tekuri/jsonschema/v6"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// ValidateAgainstSchema checks that values does not violate the structure laid out in schema
+func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error {
+ var sb strings.Builder
+ if chrt.Schema != nil {
+ slog.Debug("chart name", "chart-name", chrt.Name())
+ err := ValidateAgainstSingleSchema(values, chrt.Schema)
+ if err != nil {
+ sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name()))
+ sb.WriteString(err.Error())
+ }
+ }
+ slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies()))
+ // For each dependency, recursively call this function with the coalesced values
+ for _, subchart := range chrt.Dependencies() {
+ subchartValues := values[subchart.Name()].(map[string]interface{})
+ if err := ValidateAgainstSchema(subchart, subchartValues); err != nil {
+ sb.WriteString(err.Error())
+ }
+ }
+
+ if sb.Len() > 0 {
+ return errors.New(sb.String())
+ }
+
+ return nil
+}
+
+// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema
+func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) {
+ defer func() {
+ if r := recover(); r != nil {
+ reterr = fmt.Errorf("unable to validate schema: %s", r)
+ }
+ }()
+
+ // This unmarshal function leverages UseNumber() for number precision. The parser
+ // used for values does this as well.
+ schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON))
+ if err != nil {
+ return err
+ }
+ slog.Debug("unmarshalled JSON schema", "schema", schemaJSON)
+
+ compiler := jsonschema.NewCompiler()
+ err = compiler.AddResource("file:///values.schema.json", schema)
+ if err != nil {
+ return err
+ }
+
+ validator, err := compiler.Compile("file:///values.schema.json")
+ if err != nil {
+ return err
+ }
+
+ err = validator.Validate(values.AsMap())
+ if err != nil {
+ return JSONSchemaValidationError{err}
+ }
+
+ return nil
+}
+
+// Note, JSONSchemaValidationError is used to wrap the error from the underlying
+// validation package so that Helm has a clean interface and the validation package
+// could be replaced without changing the Helm SDK API.
+
+// JSONSchemaValidationError is the error returned when there is a schema validation
+// error.
+type JSONSchemaValidationError struct {
+ embeddedErr error
+}
+
+// Error prints the error message
+func (e JSONSchemaValidationError) Error() string {
+ errStr := e.embeddedErr.Error()
+
+ // This string prefixes all of our error details. Further up the stack of helm error message
+ // building more detail is provided to users. This is removed.
+ errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n")
+
+ // The extra new line is needed for when there are sub-charts.
+ return errStr + "\n"
+}
diff --git a/internal/chart/v3/util/jsonschema_test.go b/internal/chart/v3/util/jsonschema_test.go
new file mode 100644
index 000000000..0a3820377
--- /dev/null
+++ b/internal/chart/v3/util/jsonschema_test.go
@@ -0,0 +1,247 @@
+/*
+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 util
+
+import (
+ "os"
+ "testing"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+func TestValidateAgainstSingleSchema(t *testing.T) {
+ values, err := ReadValuesFile("./testdata/test-values.yaml")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+ schema, err := os.ReadFile("./testdata/test-values.schema.json")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+
+ if err := ValidateAgainstSingleSchema(values, schema); err != nil {
+ t.Errorf("Error validating Values against Schema: %s", err)
+ }
+}
+
+func TestValidateAgainstInvalidSingleSchema(t *testing.T) {
+ values, err := ReadValuesFile("./testdata/test-values.yaml")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+ schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+
+ var errString string
+ if err := ValidateAgainstSingleSchema(values, schema); err == nil {
+ t.Fatalf("Expected an error, but got nil")
+ } else {
+ errString = err.Error()
+ }
+
+ expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#'
+- at '': got number, want boolean or object`
+ if errString != expectedErrString {
+ t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
+ }
+}
+
+func TestValidateAgainstSingleSchemaNegative(t *testing.T) {
+ values, err := ReadValuesFile("./testdata/test-values-negative.yaml")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+ schema, err := os.ReadFile("./testdata/test-values.schema.json")
+ if err != nil {
+ t.Fatalf("Error reading JSON file: %s", err)
+ }
+
+ var errString string
+ if err := ValidateAgainstSingleSchema(values, schema); err == nil {
+ t.Fatalf("Expected an error, but got nil")
+ } else {
+ errString = err.Error()
+ }
+
+ expectedErrString := `- at '': missing property 'employmentInfo'
+- at '/age': minimum: got -5, want 0
+`
+ if errString != expectedErrString {
+ t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
+ }
+}
+
+const subchartSchema = `{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Values",
+ "type": "object",
+ "properties": {
+ "age": {
+ "description": "Age",
+ "minimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "age"
+ ]
+}
+`
+
+const subchartSchema2020 = `{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "Values",
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "contains": { "type": "string" },
+ "unevaluatedItems": { "type": "number" }
+ }
+ },
+ "required": ["data"]
+}
+`
+
+func TestValidateAgainstSchema(t *testing.T) {
+ subchartJSON := []byte(subchartSchema)
+ subchart := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "subchart",
+ },
+ Schema: subchartJSON,
+ }
+ chrt := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "chrt",
+ },
+ }
+ chrt.AddDependency(subchart)
+
+ vals := map[string]interface{}{
+ "name": "John",
+ "subchart": map[string]interface{}{
+ "age": 25,
+ },
+ }
+
+ if err := ValidateAgainstSchema(chrt, vals); err != nil {
+ t.Errorf("Error validating Values against Schema: %s", err)
+ }
+}
+
+func TestValidateAgainstSchemaNegative(t *testing.T) {
+ subchartJSON := []byte(subchartSchema)
+ subchart := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "subchart",
+ },
+ Schema: subchartJSON,
+ }
+ chrt := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "chrt",
+ },
+ }
+ chrt.AddDependency(subchart)
+
+ vals := map[string]interface{}{
+ "name": "John",
+ "subchart": map[string]interface{}{},
+ }
+
+ var errString string
+ if err := ValidateAgainstSchema(chrt, vals); err == nil {
+ t.Fatalf("Expected an error, but got nil")
+ } else {
+ errString = err.Error()
+ }
+
+ expectedErrString := `subchart:
+- at '': missing property 'age'
+`
+ if errString != expectedErrString {
+ t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
+ }
+}
+
+func TestValidateAgainstSchema2020(t *testing.T) {
+ subchartJSON := []byte(subchartSchema2020)
+ subchart := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "subchart",
+ },
+ Schema: subchartJSON,
+ }
+ chrt := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "chrt",
+ },
+ }
+ chrt.AddDependency(subchart)
+
+ vals := map[string]interface{}{
+ "name": "John",
+ "subchart": map[string]interface{}{
+ "data": []any{"hello", 12},
+ },
+ }
+
+ if err := ValidateAgainstSchema(chrt, vals); err != nil {
+ t.Errorf("Error validating Values against Schema: %s", err)
+ }
+}
+
+func TestValidateAgainstSchema2020Negative(t *testing.T) {
+ subchartJSON := []byte(subchartSchema2020)
+ subchart := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "subchart",
+ },
+ Schema: subchartJSON,
+ }
+ chrt := &chart.Chart{
+ Metadata: &chart.Metadata{
+ Name: "chrt",
+ },
+ }
+ chrt.AddDependency(subchart)
+
+ vals := map[string]interface{}{
+ "name": "John",
+ "subchart": map[string]interface{}{
+ "data": []any{12},
+ },
+ }
+
+ var errString string
+ if err := ValidateAgainstSchema(chrt, vals); err == nil {
+ t.Fatalf("Expected an error, but got nil")
+ } else {
+ errString = err.Error()
+ }
+
+ expectedErrString := `subchart:
+- at '/data': no items match contains schema
+ - at '/data/0': got number, want string
+`
+ if errString != expectedErrString {
+ t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString)
+ }
+}
diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go
new file mode 100644
index 000000000..3125cc3c9
--- /dev/null
+++ b/internal/chart/v3/util/save.go
@@ -0,0 +1,253 @@
+/*
+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 util
+
+import (
+ "archive/tar"
+ "compress/gzip"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "time"
+
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
+
+// SaveDir saves a chart as files in a directory.
+//
+// This takes the chart name, and creates a new subdirectory inside of the given dest
+// directory, writing the chart's contents to that subdirectory.
+func SaveDir(c *chart.Chart, dest string) error {
+ // Create the chart directory
+ err := validateName(c.Name())
+ if err != nil {
+ return err
+ }
+ outdir := filepath.Join(dest, c.Name())
+ if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() {
+ return fmt.Errorf("file %s already exists and is not a directory", outdir)
+ }
+ if err := os.MkdirAll(outdir, 0755); err != nil {
+ return err
+ }
+
+ // Save the chart file.
+ if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil {
+ return err
+ }
+
+ // Save values.yaml
+ for _, f := range c.Raw {
+ if f.Name == ValuesfileName {
+ vf := filepath.Join(outdir, ValuesfileName)
+ if err := writeFile(vf, f.Data); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Save values.schema.json if it exists
+ if c.Schema != nil {
+ filename := filepath.Join(outdir, SchemafileName)
+ if err := writeFile(filename, c.Schema); err != nil {
+ return err
+ }
+ }
+
+ // Save templates and files
+ for _, o := range [][]*chart.File{c.Templates, c.Files} {
+ for _, f := range o {
+ n := filepath.Join(outdir, f.Name)
+ if err := writeFile(n, f.Data); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Save dependencies
+ base := filepath.Join(outdir, ChartsDir)
+ for _, dep := range c.Dependencies() {
+ // Here, we write each dependency as a tar file.
+ if _, err := Save(dep, base); err != nil {
+ return fmt.Errorf("saving %s: %w", dep.ChartFullPath(), err)
+ }
+ }
+ return nil
+}
+
+// Save creates an archived chart to the given directory.
+//
+// This takes an existing chart and a destination directory.
+//
+// If the directory is /foo, and the chart is named bar, with version 1.0.0, this
+// will generate /foo/bar-1.0.0.tgz.
+//
+// This returns the absolute path to the chart archive file.
+func Save(c *chart.Chart, outDir string) (string, error) {
+ if err := c.Validate(); err != nil {
+ return "", fmt.Errorf("chart validation: %w", err)
+ }
+
+ filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version)
+ filename = filepath.Join(outDir, filename)
+ dir := filepath.Dir(filename)
+ if stat, err := os.Stat(dir); err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ if err2 := os.MkdirAll(dir, 0755); err2 != nil {
+ return "", err2
+ }
+ } else {
+ return "", fmt.Errorf("stat %s: %w", dir, err)
+ }
+ } else if !stat.IsDir() {
+ return "", fmt.Errorf("is not a directory: %s", dir)
+ }
+
+ f, err := os.Create(filename)
+ if err != nil {
+ return "", err
+ }
+
+ // Wrap in gzip writer
+ zipper := gzip.NewWriter(f)
+ zipper.Extra = headerBytes
+ zipper.Comment = "Helm"
+
+ // Wrap in tar writer
+ twriter := tar.NewWriter(zipper)
+ rollback := false
+ defer func() {
+ twriter.Close()
+ zipper.Close()
+ f.Close()
+ if rollback {
+ os.Remove(filename)
+ }
+ }()
+
+ if err := writeTarContents(twriter, c, ""); err != nil {
+ rollback = true
+ return filename, err
+ }
+ return filename, nil
+}
+
+func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
+ err := validateName(c.Name())
+ if err != nil {
+ return err
+ }
+ base := filepath.Join(prefix, c.Name())
+
+ // Save Chart.yaml
+ cdata, err := yaml.Marshal(c.Metadata)
+ if err != nil {
+ return err
+ }
+ if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil {
+ return err
+ }
+
+ // Save Chart.lock
+ if c.Lock != nil {
+ ldata, err := yaml.Marshal(c.Lock)
+ if err != nil {
+ return err
+ }
+ if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil {
+ return err
+ }
+ }
+
+ // Save values.yaml
+ for _, f := range c.Raw {
+ if f.Name == ValuesfileName {
+ if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Save values.schema.json if it exists
+ if c.Schema != nil {
+ if !json.Valid(c.Schema) {
+ return errors.New("invalid JSON in " + SchemafileName)
+ }
+ if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil {
+ return err
+ }
+ }
+
+ // Save templates
+ for _, f := range c.Templates {
+ n := filepath.Join(base, f.Name)
+ if err := writeToTar(out, n, f.Data); err != nil {
+ return err
+ }
+ }
+
+ // Save files
+ for _, f := range c.Files {
+ n := filepath.Join(base, f.Name)
+ if err := writeToTar(out, n, f.Data); err != nil {
+ return err
+ }
+ }
+
+ // Save dependencies
+ for _, dep := range c.Dependencies() {
+ if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// writeToTar writes a single file to a tar archive.
+func writeToTar(out *tar.Writer, name string, body []byte) error {
+ // TODO: Do we need to create dummy parent directory names if none exist?
+ h := &tar.Header{
+ Name: filepath.ToSlash(name),
+ Mode: 0644,
+ Size: int64(len(body)),
+ ModTime: time.Now(),
+ }
+ if err := out.WriteHeader(h); err != nil {
+ return err
+ }
+ _, err := out.Write(body)
+ return err
+}
+
+// If the name has directory name has characters which would change the location
+// they need to be removed.
+func validateName(name string) error {
+ nname := filepath.Base(name)
+
+ if nname != name {
+ return ErrInvalidChartName{name}
+ }
+
+ return nil
+}
diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go
new file mode 100644
index 000000000..852675bb0
--- /dev/null
+++ b/internal/chart/v3/util/save_test.go
@@ -0,0 +1,261 @@
+/*
+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 util
+
+import (
+ "archive/tar"
+ "bytes"
+ "compress/gzip"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+ "helm.sh/helm/v4/internal/chart/v3/loader"
+)
+
+func TestSave(t *testing.T) {
+ tmp := t.TempDir()
+
+ for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} {
+ t.Run("outDir="+dest, func(t *testing.T) {
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{
+ APIVersion: chart.APIVersionV3,
+ Name: "ahab",
+ Version: "1.2.3",
+ },
+ Lock: &chart.Lock{
+ Digest: "testdigest",
+ },
+ Files: []*chart.File{
+ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
+ },
+ Schema: []byte("{\n \"title\": \"Values\"\n}"),
+ }
+ chartWithInvalidJSON := withSchema(*c, []byte("{"))
+
+ where, err := Save(c, dest)
+ if err != nil {
+ t.Fatalf("Failed to save: %s", err)
+ }
+ if !strings.HasPrefix(where, dest) {
+ t.Fatalf("Expected %q to start with %q", where, dest)
+ }
+ if !strings.HasSuffix(where, ".tgz") {
+ t.Fatalf("Expected %q to end with .tgz", where)
+ }
+
+ c2, err := loader.LoadFile(where)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c2.Name() != c.Name() {
+ t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name())
+ }
+ if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" {
+ t.Fatal("Files data did not match")
+ }
+
+ if !bytes.Equal(c.Schema, c2.Schema) {
+ indentation := 4
+ formattedExpected := Indent(indentation, string(c.Schema))
+ formattedActual := Indent(indentation, string(c2.Schema))
+ t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual)
+ }
+ if _, err := Save(&chartWithInvalidJSON, dest); err == nil {
+ t.Fatalf("Invalid JSON was not caught while saving chart")
+ }
+
+ c.Metadata.APIVersion = chart.APIVersionV3
+ where, err = Save(c, dest)
+ if err != nil {
+ t.Fatalf("Failed to save: %s", err)
+ }
+ c2, err = loader.LoadFile(where)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if c2.Lock == nil {
+ t.Fatal("Expected v3 chart archive to contain a Chart.lock file")
+ }
+ if c2.Lock.Digest != c.Lock.Digest {
+ t.Fatal("Chart.lock data did not match")
+ }
+ })
+ }
+
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{
+ APIVersion: chart.APIVersionV3,
+ Name: "../ahab",
+ Version: "1.2.3",
+ },
+ Lock: &chart.Lock{
+ Digest: "testdigest",
+ },
+ Files: []*chart.File{
+ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
+ },
+ }
+ _, err := Save(c, tmp)
+ if err == nil {
+ t.Fatal("Expected error saving chart with invalid name")
+ }
+}
+
+// Creates a copy with a different schema; does not modify anything.
+func withSchema(chart chart.Chart, schema []byte) chart.Chart {
+ chart.Schema = schema
+ return chart
+}
+
+func Indent(n int, text string) string {
+ startOfLine := regexp.MustCompile(`(?m)^`)
+ indentation := strings.Repeat(" ", n)
+ return startOfLine.ReplaceAllLiteralString(text, indentation)
+}
+
+func TestSavePreservesTimestamps(t *testing.T) {
+ // Test executes so quickly that if we don't subtract a second, the
+ // check will fail because `initialCreateTime` will be identical to the
+ // written timestamp for the files.
+ initialCreateTime := time.Now().Add(-1 * time.Second)
+
+ tmp := t.TempDir()
+
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{
+ APIVersion: chart.APIVersionV3,
+ Name: "ahab",
+ Version: "1.2.3",
+ },
+ Values: map[string]interface{}{
+ "imageName": "testimage",
+ "imageId": 42,
+ },
+ Files: []*chart.File{
+ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
+ },
+ Schema: []byte("{\n \"title\": \"Values\"\n}"),
+ }
+
+ where, err := Save(c, tmp)
+ if err != nil {
+ t.Fatalf("Failed to save: %s", err)
+ }
+
+ allHeaders, err := retrieveAllHeadersFromTar(where)
+ if err != nil {
+ t.Fatalf("Failed to parse tar: %v", err)
+ }
+
+ for _, header := range allHeaders {
+ if header.ModTime.Before(initialCreateTime) {
+ t.Fatalf("File timestamp not preserved: %v", header.ModTime)
+ }
+ }
+}
+
+// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function
+// as well, so we are not duplicating components of the code which iterate
+// through the tar.
+func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) {
+ raw, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer raw.Close()
+
+ unzipped, err := gzip.NewReader(raw)
+ if err != nil {
+ return nil, err
+ }
+ defer unzipped.Close()
+
+ tr := tar.NewReader(unzipped)
+ headers := []*tar.Header{}
+ for {
+ hd, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ headers = append(headers, hd)
+ }
+
+ return headers, nil
+}
+
+func TestSaveDir(t *testing.T) {
+ tmp := t.TempDir()
+
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{
+ APIVersion: chart.APIVersionV3,
+ Name: "ahab",
+ Version: "1.2.3",
+ },
+ Files: []*chart.File{
+ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
+ },
+ Templates: []*chart.File{
+ {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")},
+ },
+ }
+
+ if err := SaveDir(c, tmp); err != nil {
+ t.Fatalf("Failed to save: %s", err)
+ }
+
+ c2, err := loader.LoadDir(tmp + "/ahab")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if c2.Name() != c.Name() {
+ t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name())
+ }
+
+ if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name {
+ t.Fatal("Templates data did not match")
+ }
+
+ if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name {
+ t.Fatal("Files data did not match")
+ }
+
+ tmp2 := t.TempDir()
+ c.Metadata.Name = "../ahab"
+ pth := filepath.Join(tmp2, "tmpcharts")
+ if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" {
+ t.Fatalf("Did not get expected error for chart named %q", c.Name())
+ }
+}
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml
new file mode 100644
index 000000000..4a4da7996
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml
@@ -0,0 +1,14 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: chart-with-dependency-aliased-twice
+type: application
+version: 1.0.0
+
+dependencies:
+ - name: child
+ alias: foo
+ version: 1.0.0
+ - name: child
+ alias: bar
+ version: 1.0.0
+
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml
new file mode 100644
index 000000000..0f3afd8c6
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: child
+type: application
+version: 1.0.0
+
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml
new file mode 100644
index 000000000..3e0bf725b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: grandchild
+type: application
+version: 1.0.0
+
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml
new file mode 100644
index 000000000..1830492ef
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Chart.Name }}-{{ .Values.from }}
+data:
+ {{- toYaml .Values | nindent 2 }}
+
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml
new file mode 100644
index 000000000..b5d55af7c
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Chart.Name }}
+data:
+ {{- toYaml .Values | nindent 2 }}
+
diff --git a/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml
new file mode 100644
index 000000000..695521a4a
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml
@@ -0,0 +1,7 @@
+foo:
+ grandchild:
+ from: foo
+bar:
+ grandchild:
+ from: bar
+
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml
new file mode 100644
index 000000000..f2f0610b5
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml
@@ -0,0 +1,20 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: chart-with-dependency-aliased-twice
+type: application
+version: 1.0.0
+
+dependencies:
+ - name: child
+ alias: foo
+ version: 1.0.0
+ import-values:
+ - parent: foo-defaults
+ child: defaults
+ - name: child
+ alias: bar
+ version: 1.0.0
+ import-values:
+ - parent: bar-defaults
+ child: defaults
+
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml
new file mode 100644
index 000000000..08ccac9e5
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml
@@ -0,0 +1,12 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: child
+type: application
+version: 1.0.0
+
+dependencies:
+ - name: grandchild
+ version: 1.0.0
+ import-values:
+ - parent: defaults
+ child: defaults
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml
new file mode 100644
index 000000000..3e0bf725b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v3
+appVersion: 1.0.0
+name: grandchild
+type: application
+version: 1.0.0
+
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml
new file mode 100644
index 000000000..f51c594f4
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml
@@ -0,0 +1,2 @@
+defaults:
+ defaultValue: "42"
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml
new file mode 100644
index 000000000..3140f53dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Chart.Name }}
+data:
+ {{ .Values.defaults | toYaml }}
+
diff --git a/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml
new file mode 100644
index 000000000..a2b62c95a
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Chart.Name }}
+data:
+ {{ toYaml .Values.defaults | indent 2 }}
+
diff --git a/internal/chart/v3/util/testdata/chartfiletest.yaml b/internal/chart/v3/util/testdata/chartfiletest.yaml
new file mode 100644
index 000000000..d222c8f8d
--- /dev/null
+++ b/internal/chart/v3/util/testdata/chartfiletest.yaml
@@ -0,0 +1,20 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
diff --git a/internal/chart/v3/util/testdata/coleridge.yaml b/internal/chart/v3/util/testdata/coleridge.yaml
new file mode 100644
index 000000000..b6579628b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/coleridge.yaml
@@ -0,0 +1,12 @@
+poet: "Coleridge"
+title: "Rime of the Ancient Mariner"
+stanza: ["at", "length", "did", "cross", "an", "Albatross"]
+
+mariner:
+ with: "crossbow"
+ shot: "ALBATROSS"
+
+water:
+ water:
+ where: "everywhere"
+ nor: "any drop to drink"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock
new file mode 100644
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml
new file mode 100644
index 000000000..b8773d0d3
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml
@@ -0,0 +1,29 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+ alias: mariners2
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+ alias: mariners1
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..3190136b0
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore
new file mode 100644
index 000000000..8a71bc82e
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore
@@ -0,0 +1,2 @@
+ignore/
+.*
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml
new file mode 100644
index 000000000..8b4ad8cdd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml
@@ -0,0 +1,17 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml
new file mode 100644
index 000000000..8b4ad8cdd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml
@@ -0,0 +1,17 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..3190136b0
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml
new file mode 100644
index 000000000..06283093e
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..3190136b0
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml
new file mode 100644
index 000000000..6543799d0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml
@@ -0,0 +1,21 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz
new file mode 100644
index 000000000..3190136b0
Binary files /dev/null and b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz differ
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz b/internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz
new file mode 100644
index 000000000..8731dce02
Binary files /dev/null and b/internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz differ
diff --git a/internal/chart/v3/util/testdata/frobnitz/.helmignore b/internal/chart/v3/util/testdata/frobnitz/.helmignore
new file mode 100644
index 000000000..9973a57b8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/.helmignore
@@ -0,0 +1 @@
+ignore/
diff --git a/internal/chart/v3/util/testdata/frobnitz/Chart.lock b/internal/chart/v3/util/testdata/frobnitz/Chart.lock
new file mode 100644
index 000000000..6fcc2ed9f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/Chart.lock
@@ -0,0 +1,8 @@
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
+digest: invalid
diff --git a/internal/chart/v3/util/testdata/frobnitz/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/Chart.yaml
new file mode 100644
index 000000000..1b63fc3e2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v3
+name: frobnitz
+description: This is a frobnitz.
+version: "1.2.3"
+keywords:
+ - frobnitz
+ - sprocket
+ - dodad
+maintainers:
+ - name: The Helm Team
+ email: helm@example.com
+ - name: Someone Else
+ email: nobody@example.com
+sources:
+ - https://example.com/foo/bar
+home: http://example.com
+icon: https://example.com/64x64.png
+annotations:
+ extrakey: extravalue
+ anotherkey: anothervalue
+dependencies:
+ - name: alpine
+ version: "0.1.0"
+ repository: https://example.com/charts
+ - name: mariner
+ version: "4.3.2"
+ repository: https://example.com/charts
diff --git a/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt b/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt
new file mode 100644
index 000000000..2010438c2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt
@@ -0,0 +1 @@
+This is an install document. The client may display this.
diff --git a/internal/chart/v3/util/testdata/frobnitz/LICENSE b/internal/chart/v3/util/testdata/frobnitz/LICENSE
new file mode 100644
index 000000000..6121943b1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/LICENSE
@@ -0,0 +1 @@
+LICENSE placeholder.
diff --git a/internal/chart/v3/util/testdata/frobnitz/README.md b/internal/chart/v3/util/testdata/frobnitz/README.md
new file mode 100644
index 000000000..8cf4cc3d7
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/README.md
@@ -0,0 +1,11 @@
+# Frobnitz
+
+This is an example chart.
+
+## Usage
+
+This is an example. It has no usage.
+
+## Development
+
+For developer info, see the top-level repository.
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me b/internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me
new file mode 100644
index 000000000..2cecca682
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me
@@ -0,0 +1 @@
+This should be ignored by the loader, but may be included in a chart.
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml
new file mode 100644
index 000000000..2a2c9c883
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: alpine
+description: Deploy a basic Alpine Linux pod
+version: 0.1.0
+home: https://helm.sh/helm
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md
new file mode 100644
index 000000000..b30b949dd
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md
@@ -0,0 +1,9 @@
+This example was generated using the command `helm create alpine`.
+
+The `templates/` directory contains a very simple pod resource with a
+couple of parameters.
+
+The `values.toml` file contains the default values for the
+`alpine-pod.yaml` template.
+
+You can install this example using `helm install ./alpine`.
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml
new file mode 100644
index 000000000..aea109c75
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: mast1
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml
new file mode 100644
index 000000000..42c39c262
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml
@@ -0,0 +1,4 @@
+# Default values for mast1.
+# This is a YAML-formatted file.
+# Declare name/value pairs to be passed into your templates.
+# name = "value"
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz
new file mode 100644
index 000000000..61cb62051
Binary files /dev/null and b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml
new file mode 100644
index 000000000..5bbae10af
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: {{.Release.Name}}-{{.Chart.Name}}
+ labels:
+ app.kubernetes.io/managed-by: {{.Release.Service}}
+ chartName: {{.Chart.Name}}
+ chartVersion: {{.Chart.Version | quote}}
+spec:
+ restartPolicy: {{default "Never" .restart_policy}}
+ containers:
+ - name: waiter
+ image: "alpine:3.3"
+ command: ["/bin/sleep","9000"]
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml
new file mode 100644
index 000000000..6c2aab7ba
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml
@@ -0,0 +1,2 @@
+# The pod name
+name: "my-alpine"
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml
new file mode 100644
index 000000000..4d3eea730
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml
@@ -0,0 +1,9 @@
+apiVersion: v3
+name: mariner
+description: A Helm chart for Kubernetes
+version: 4.3.2
+home: ""
+dependencies:
+ - name: albatross
+ repository: https://example.com/mariner/charts
+ version: "0.1.0"
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml
new file mode 100644
index 000000000..da605991b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: albatross
+description: A Helm chart for Kubernetes
+version: 0.1.0
+home: ""
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml
new file mode 100644
index 000000000..3121cd7ce
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml
@@ -0,0 +1,4 @@
+albatross: "true"
+
+global:
+ author: Coleridge
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl
new file mode 100644
index 000000000..29c11843a
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl
@@ -0,0 +1 @@
+# This is a placeholder.
diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml
new file mode 100644
index 000000000..b0ccb0086
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml
@@ -0,0 +1,7 @@
+# Default values for .
+# This is a YAML-formatted file. https://github.com/toml-lang/toml
+# Declare name/value pairs to be passed into your templates.
+# name: "value"
+
+:
+ test: true
diff --git a/internal/chart/v3/util/testdata/frobnitz/docs/README.md b/internal/chart/v3/util/testdata/frobnitz/docs/README.md
new file mode 100644
index 000000000..d40747caf
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/docs/README.md
@@ -0,0 +1 @@
+This is a placeholder for documentation.
diff --git a/internal/chart/v3/util/testdata/frobnitz/icon.svg b/internal/chart/v3/util/testdata/frobnitz/icon.svg
new file mode 100644
index 000000000..892130606
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/icon.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/internal/chart/v3/util/testdata/frobnitz/ignore/me.txt b/internal/chart/v3/util/testdata/frobnitz/ignore/me.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl b/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl
new file mode 100644
index 000000000..c651ee6a0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl
@@ -0,0 +1 @@
+Hello {{.Name | default "world"}}
diff --git a/internal/chart/v3/util/testdata/frobnitz/values.yaml b/internal/chart/v3/util/testdata/frobnitz/values.yaml
new file mode 100644
index 000000000..61f501258
--- /dev/null
+++ b/internal/chart/v3/util/testdata/frobnitz/values.yaml
@@ -0,0 +1,6 @@
+# A values file contains configuration.
+
+name: "Some Name"
+
+section:
+ name: "Name in a section"
diff --git a/internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz b/internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz
new file mode 100644
index 000000000..692965951
Binary files /dev/null and b/internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz differ
diff --git a/internal/chart/v3/util/testdata/genfrob.sh b/internal/chart/v3/util/testdata/genfrob.sh
new file mode 100755
index 000000000..35fdd59f2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/genfrob.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+# Pack the albatross chart into the mariner chart.
+echo "Packing albatross into mariner"
+tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross
+
+echo "Packing mariner into frobnitz"
+tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner
+tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner
+
+# Pack the frobnitz chart.
+echo "Packing frobnitz"
+tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz
+tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock
new file mode 100644
index 000000000..b2f17fb39
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock
@@ -0,0 +1,9 @@
+dependencies:
+- name: dev
+ repository: file://envs/dev
+ version: v0.1.0
+- name: prod
+ repository: file://envs/prod
+ version: v0.1.0
+digest: sha256:9403fc24f6cf9d6055820126cf7633b4bd1fed3c77e4880c674059f536346182
+generated: "2020-02-03T10:38:51.180474+01:00"
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml
new file mode 100644
index 000000000..0b3e9958b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml
@@ -0,0 +1,22 @@
+apiVersion: v3
+name: parent-chart
+version: v0.1.0
+appVersion: v0.1.0
+dependencies:
+ - name: dev
+ repository: "file://envs/dev"
+ version: ">= 0.0.1"
+ condition: dev.enabled,global.dev.enabled
+ tags:
+ - dev
+ import-values:
+ - data
+
+ - name: prod
+ repository: "file://envs/prod"
+ version: ">= 0.0.1"
+ condition: prod.enabled,global.prod.enabled
+ tags:
+ - prod
+ import-values:
+ - data
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz
new file mode 100644
index 000000000..d28e1621c
Binary files /dev/null and b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz
new file mode 100644
index 000000000..a0c5aa84b
Binary files /dev/null and b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz differ
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml
new file mode 100644
index 000000000..72427c097
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+name: dev
+version: v0.1.0
+appVersion: v0.1.0
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml
new file mode 100644
index 000000000..38f03484d
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml
@@ -0,0 +1,9 @@
+# Dev values parent-chart
+nameOverride: parent-chart-dev
+exports:
+ data:
+ resources:
+ autoscaler:
+ minReplicas: 1
+ maxReplicas: 3
+ targetCPUUtilizationPercentage: 80
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml
new file mode 100644
index 000000000..058ab3942
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+name: prod
+version: v0.1.0
+appVersion: v0.1.0
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml
new file mode 100644
index 000000000..10cc756b2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml
@@ -0,0 +1,9 @@
+# Prod values parent-chart
+nameOverride: parent-chart-prod
+exports:
+ data:
+ resources:
+ autoscaler:
+ minReplicas: 2
+ maxReplicas: 5
+ targetCPUUtilizationPercentage: 90
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml
new file mode 100644
index 000000000..976e5a8f1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml
@@ -0,0 +1,16 @@
+###################################################################################################
+# parent-chart horizontal pod autoscaler
+###################################################################################################
+apiVersion: autoscaling/v1
+kind: HorizontalPodAutoscaler
+metadata:
+ name: {{ .Release.Name }}-autoscaler
+ namespace: {{ .Release.Namespace }}
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1beta1
+ kind: Deployment
+ name: {{ .Release.Name }}
+ minReplicas: {{ required "A valid .Values.resources.autoscaler.minReplicas entry required!" .Values.resources.autoscaler.minReplicas }}
+ maxReplicas: {{ required "A valid .Values.resources.autoscaler.maxReplicas entry required!" .Values.resources.autoscaler.maxReplicas }}
+ targetCPUUtilizationPercentage: {{ required "A valid .Values.resources.autoscaler.targetCPUUtilizationPercentage!" .Values.resources.autoscaler.targetCPUUtilizationPercentage }}
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml
new file mode 100644
index 000000000..b812f0a33
--- /dev/null
+++ b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml
@@ -0,0 +1,10 @@
+# Default values for parent-chart.
+nameOverride: parent-chart
+tags:
+ dev: false
+ prod: true
+resources:
+ autoscaler:
+ minReplicas: 0
+ maxReplicas: 0
+ targetCPUUtilizationPercentage: 99
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/joonix/Chart.yaml b/internal/chart/v3/util/testdata/joonix/Chart.yaml
new file mode 100644
index 000000000..1860a3df1
--- /dev/null
+++ b/internal/chart/v3/util/testdata/joonix/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: joonix
+version: 1.2.3
diff --git a/internal/chart/v3/util/testdata/joonix/charts/.gitkeep b/internal/chart/v3/util/testdata/joonix/charts/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/internal/chart/v3/util/testdata/subpop/Chart.yaml b/internal/chart/v3/util/testdata/subpop/Chart.yaml
new file mode 100644
index 000000000..53e9ec502
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/Chart.yaml
@@ -0,0 +1,41 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: parentchart
+version: 0.1.0
+dependencies:
+ - name: subchart1
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchart1.enabled
+ tags:
+ - front-end
+ - subchart1
+ import-values:
+ - child: SC1data
+ parent: imported-chart1
+ - child: SC1data
+ parent: overridden-chart1
+ - child: imported-chartA
+ parent: imported-chartA
+ - child: imported-chartA-B
+ parent: imported-chartA-B
+ - child: overridden-chartA-B
+ parent: overridden-chartA-B
+ - child: SCBexported1A
+ parent: .
+ - SCBexported2
+ - SC1exported1
+
+ - name: subchart2
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchart2.enabled
+ tags:
+ - back-end
+ - subchart2
+
+ - name: subchart2
+ alias: subchart2alias
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchart2alias.enabled
diff --git a/internal/chart/v3/util/testdata/subpop/README.md b/internal/chart/v3/util/testdata/subpop/README.md
new file mode 100644
index 000000000..e43fbfe9c
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/README.md
@@ -0,0 +1,18 @@
+## Subpop
+
+This chart is for testing the processing of enabled/disabled charts
+via conditions and tags.
+
+Currently there are three levels:
+
+````
+parent
+-1 tags: front-end, subchart1
+--A tags: front-end, subchartA
+--B tags: front-end, subchartB
+-2 tags: back-end, subchart2
+--B tags: back-end, subchartB
+--C tags: back-end, subchartC
+````
+
+Tags and conditions are currently in requirements.yaml files.
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml
new file mode 100644
index 000000000..1539fb97d
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml
@@ -0,0 +1,36 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subchart1
+version: 0.1.0
+dependencies:
+ - name: subcharta
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subcharta.enabled
+ tags:
+ - front-end
+ - subcharta
+ import-values:
+ - child: SCAdata
+ parent: imported-chartA
+ - child: SCAdata
+ parent: overridden-chartA
+ - child: SCAdata
+ parent: imported-chartA-B
+
+ - name: subchartb
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchartb.enabled
+ import-values:
+ - child: SCBdata
+ parent: imported-chartB
+ - child: SCBdata
+ parent: imported-chartA-B
+ - child: exports.SCBexported2
+ parent: exports.SCBexported2
+ - SCBexported1
+
+ tags:
+ - front-end
+ - subchartb
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml
new file mode 100644
index 000000000..2755a821b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subcharta
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml
new file mode 100644
index 000000000..27501e1e0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml
new file mode 100644
index 000000000..f0381ae6a
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml
@@ -0,0 +1,17 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+# subchartA
+service:
+ name: apache
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+SCAdata:
+ SCAbool: false
+ SCAfloat: 3.1
+ SCAint: 55
+ SCAstring: "jabba"
+ SCAnested1:
+ SCAnested2: true
+
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml
new file mode 100644
index 000000000..bf12fe8f3
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subchartb
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml
new file mode 100644
index 000000000..27501e1e0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml
new file mode 100644
index 000000000..774fdd75c
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml
@@ -0,0 +1,35 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+
+SCBdata:
+ SCBbool: true
+ SCBfloat: 7.77
+ SCBint: 33
+ SCBstring: "boba"
+
+exports:
+ SCBexported1:
+ SCBexported1A:
+ SCBexported1B: 1965
+
+ SCBexported2:
+ SCBexported2A: "blaster"
+
+global:
+ kolla:
+ nova:
+ api:
+ all:
+ port: 8774
+ metadata:
+ all:
+ port: 8775
+
+
+
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml
new file mode 100644
index 000000000..fca77fd4b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml
@@ -0,0 +1,13 @@
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+ name: testCRDs
+spec:
+ group: testCRDGroups
+ names:
+ kind: TestCRD
+ listKind: TestCRDList
+ plural: TestCRDs
+ shortNames:
+ - tc
+ singular: authconfig
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt
new file mode 100644
index 000000000..4bdf443f6
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt
@@ -0,0 +1 @@
+Sample notes for {{ .Chart.Name }}
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml
new file mode 100644
index 000000000..fee94dced
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml
@@ -0,0 +1,22 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+ app.kubernetes.io/instance: "{{ .Release.Name }}"
+ kube-version/major: "{{ .Capabilities.KubeVersion.Major }}"
+ kube-version/minor: "{{ .Capabilities.KubeVersion.Minor }}"
+ kube-version/version: "v{{ .Capabilities.KubeVersion.Major }}.{{ .Capabilities.KubeVersion.Minor }}.0"
+{{- if .Capabilities.APIVersions.Has "helm.k8s.io/test" }}
+ kube-api-version/test: v1
+{{- end }}
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml
new file mode 100644
index 000000000..91b954e5f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml
@@ -0,0 +1,7 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ .Chart.Name }}-role
+rules:
+- resources: ["*"]
+ verbs: ["get","list","watch"]
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml
new file mode 100644
index 000000000..5d193f1a6
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ .Chart.Name }}-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: {{ .Chart.Name }}-role
+subjects:
+- kind: ServiceAccount
+ name: {{ .Chart.Name }}-sa
+ namespace: default
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml
new file mode 100644
index 000000000..7126c7d89
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ .Chart.Name }}-sa
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml
new file mode 100644
index 000000000..a974e316a
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml
@@ -0,0 +1,55 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+# subchart1
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+
+
+SC1data:
+ SC1bool: true
+ SC1float: 3.14
+ SC1int: 100
+ SC1string: "dollywood"
+ SC1extra1: 11
+
+imported-chartA:
+ SC1extra2: 1.337
+
+overridden-chartA:
+ SCAbool: true
+ SCAfloat: 3.14
+ SCAint: 100
+ SCAstring: "jabbathehut"
+ SC1extra3: true
+
+imported-chartA-B:
+ SC1extra5: "tiller"
+
+overridden-chartA-B:
+ SCAbool: true
+ SCAfloat: 3.33
+ SCAint: 555
+ SCAstring: "wormwood"
+ SCAextra1: 23
+
+ SCBbool: true
+ SCBfloat: 0.25
+ SCBint: 98
+ SCBstring: "murkwood"
+ SCBextra1: 13
+
+ SC1extra6: 77
+
+SCBexported1A:
+ SC1extra7: true
+
+exports:
+ SC1exported1:
+ global:
+ SC1exported2:
+ all:
+ SC1exported3: "SC1expstr"
\ No newline at end of file
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml
new file mode 100644
index 000000000..e77657040
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml
@@ -0,0 +1,19 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subchart2
+version: 0.1.0
+dependencies:
+ - name: subchartb
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchartb.enabled
+ tags:
+ - back-end
+ - subchartb
+ - name: subchartc
+ repository: http://localhost:10191
+ version: 0.1.0
+ condition: subchartc.enabled
+ tags:
+ - back-end
+ - subchartc
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml
new file mode 100644
index 000000000..bf12fe8f3
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subchartb
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml
new file mode 100644
index 000000000..fb3dfc445
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: subchart2-{{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: subchart2-{{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml
new file mode 100644
index 000000000..5e5b21065
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml
@@ -0,0 +1,21 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image:
+ repository: nginx
+ tag: stable
+ pullPolicy: IfNotPresent
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+resources:
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml
new file mode 100644
index 000000000..e8c0ef5e5
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: subchartc
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml
new file mode 100644
index 000000000..27501e1e0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml
new file mode 100644
index 000000000..5e5b21065
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml
@@ -0,0 +1,21 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image:
+ repository: nginx
+ tag: stable
+ pullPolicy: IfNotPresent
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+resources:
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml
new file mode 100644
index 000000000..27501e1e0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml
new file mode 100644
index 000000000..5e5b21065
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml
@@ -0,0 +1,21 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image:
+ repository: nginx
+ tag: stable
+ pullPolicy: IfNotPresent
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+resources:
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
diff --git a/internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml
new file mode 100644
index 000000000..09eb05a96
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v3
+description: A Helm chart for Kubernetes
+name: parentchart
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml
new file mode 100644
index 000000000..27501e1e0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Chart.Name }}
+ labels:
+ helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.externalPort }}
+ targetPort: {{ .Values.service.internalPort }}
+ protocol: TCP
+ name: {{ .Values.service.name }}
+ selector:
+ app.kubernetes.io/name: {{ .Chart.Name }}
diff --git a/internal/chart/v3/util/testdata/subpop/noreqs/values.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/values.yaml
new file mode 100644
index 000000000..4ed3b7ad3
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/noreqs/values.yaml
@@ -0,0 +1,26 @@
+# Default values for subchart.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image:
+ repository: nginx
+ tag: stable
+ pullPolicy: IfNotPresent
+service:
+ name: nginx
+ type: ClusterIP
+ externalPort: 80
+ internalPort: 80
+resources:
+ limits:
+ cpu: 100m
+ memory: 128Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+
+
+# switch-like
+tags:
+ front-end: true
+ back-end: false
diff --git a/internal/chart/v3/util/testdata/subpop/values.yaml b/internal/chart/v3/util/testdata/subpop/values.yaml
new file mode 100644
index 000000000..ba70ed406
--- /dev/null
+++ b/internal/chart/v3/util/testdata/subpop/values.yaml
@@ -0,0 +1,45 @@
+# parent/values.yaml
+
+imported-chart1:
+ SPextra1: "helm rocks"
+
+overridden-chart1:
+ SC1bool: false
+ SC1float: 3.141592
+ SC1int: 99
+ SC1string: "pollywog"
+ SPextra2: 42
+
+
+imported-chartA:
+ SPextra3: 1.337
+
+overridden-chartA:
+ SCAbool: true
+ SCAfloat: 41.3
+ SCAint: 808
+ SCAstring: "jabberwocky"
+ SPextra4: true
+
+imported-chartA-B:
+ SPextra5: "k8s"
+
+overridden-chartA-B:
+ SCAbool: true
+ SCAfloat: 41.3
+ SCAint: 808
+ SCAstring: "jabberwocky"
+ SCBbool: false
+ SCBfloat: 1.99
+ SCBint: 77
+ SCBstring: "jango"
+ SPextra6: 111
+
+tags:
+ front-end: true
+ back-end: false
+
+subchart2alias:
+ enabled: false
+
+ensurenull: null
diff --git a/internal/chart/v3/util/testdata/test-values-invalid.schema.json b/internal/chart/v3/util/testdata/test-values-invalid.schema.json
new file mode 100644
index 000000000..35a16a2c4
--- /dev/null
+++ b/internal/chart/v3/util/testdata/test-values-invalid.schema.json
@@ -0,0 +1 @@
+ 1E1111111
diff --git a/internal/chart/v3/util/testdata/test-values-negative.yaml b/internal/chart/v3/util/testdata/test-values-negative.yaml
new file mode 100644
index 000000000..5a1250bff
--- /dev/null
+++ b/internal/chart/v3/util/testdata/test-values-negative.yaml
@@ -0,0 +1,14 @@
+firstname: John
+lastname: Doe
+age: -5
+likesCoffee: true
+addresses:
+ - city: Springfield
+ street: Main
+ number: 12345
+ - city: New York
+ street: Broadway
+ number: 67890
+phoneNumbers:
+ - "(888) 888-8888"
+ - "(555) 555-5555"
diff --git a/internal/chart/v3/util/testdata/test-values.schema.json b/internal/chart/v3/util/testdata/test-values.schema.json
new file mode 100644
index 000000000..4df89bbe8
--- /dev/null
+++ b/internal/chart/v3/util/testdata/test-values.schema.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "addresses": {
+ "description": "List of addresses",
+ "items": {
+ "properties": {
+ "city": {
+ "type": "string"
+ },
+ "number": {
+ "type": "number"
+ },
+ "street": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "age": {
+ "description": "Age",
+ "minimum": 0,
+ "type": "integer"
+ },
+ "employmentInfo": {
+ "properties": {
+ "salary": {
+ "minimum": 0,
+ "type": "number"
+ },
+ "title": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "salary"
+ ],
+ "type": "object"
+ },
+ "firstname": {
+ "description": "First name",
+ "type": "string"
+ },
+ "lastname": {
+ "type": "string"
+ },
+ "likesCoffee": {
+ "type": "boolean"
+ },
+ "phoneNumbers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "firstname",
+ "lastname",
+ "addresses",
+ "employmentInfo"
+ ],
+ "title": "Values",
+ "type": "object"
+}
diff --git a/internal/chart/v3/util/testdata/test-values.yaml b/internal/chart/v3/util/testdata/test-values.yaml
new file mode 100644
index 000000000..042dea664
--- /dev/null
+++ b/internal/chart/v3/util/testdata/test-values.yaml
@@ -0,0 +1,17 @@
+firstname: John
+lastname: Doe
+age: 25
+likesCoffee: true
+employmentInfo:
+ title: Software Developer
+ salary: 100000
+addresses:
+ - city: Springfield
+ street: Main
+ number: 12345
+ - city: New York
+ street: Broadway
+ number: 67890
+phoneNumbers:
+ - "(888) 888-8888"
+ - "(555) 555-5555"
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/README.md b/internal/chart/v3/util/testdata/three-level-dependent-chart/README.md
new file mode 100644
index 000000000..536bb9792
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/README.md
@@ -0,0 +1,16 @@
+# Three Level Dependent Chart
+
+This chart is for testing the processing of multi-level dependencies.
+
+Consists of the following charts:
+
+- Library Chart
+- App Chart (Uses Library Chart as dependency, 2x: app1/app2)
+- Umbrella Chart (Has all the app charts as dependencies)
+
+The precedence is as follows: `library < app < umbrella`
+
+Catches two use-cases:
+
+- app overwriting library (app2)
+- umbrella overwriting app and library (app1)
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml
new file mode 100644
index 000000000..1026f8901
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml
@@ -0,0 +1,19 @@
+apiVersion: v3
+name: umbrella
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+
+dependencies:
+- name: app1
+ version: 0.1.0
+ condition: app1.enabled
+- name: app2
+ version: 0.1.0
+ condition: app2.enabled
+- name: app3
+ version: 0.1.0
+ condition: app3.enabled
+- name: app4
+ version: 0.1.0
+ condition: app4.enabled
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml
new file mode 100644
index 000000000..5bdf21570
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml
@@ -0,0 +1,11 @@
+apiVersion: v3
+name: app1
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+
+dependencies:
+- name: library
+ version: 0.1.0
+ import-values:
+ - defaults
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml
new file mode 100644
index 000000000..9bc306361
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: library
+description: A Helm chart for Kubernetes
+type: library
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml
new file mode 100644
index 000000000..3fd398b53
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Service
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml
new file mode 100644
index 000000000..0c08b6cd2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml
@@ -0,0 +1,5 @@
+exports:
+ defaults:
+ service:
+ type: ClusterIP
+ port: 9090
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml
new file mode 100644
index 000000000..8ed8ddf1f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml
@@ -0,0 +1 @@
+{{- include "library.service" . }}
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml
new file mode 100644
index 000000000..3728aa930
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml
@@ -0,0 +1,3 @@
+service:
+ type: ClusterIP
+ port: 1234
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml
new file mode 100644
index 000000000..1313ce4e9
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml
@@ -0,0 +1,11 @@
+apiVersion: v3
+name: app2
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+
+dependencies:
+- name: library
+ version: 0.1.0
+ import-values:
+ - defaults
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml
new file mode 100644
index 000000000..9bc306361
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: library
+description: A Helm chart for Kubernetes
+type: library
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml
new file mode 100644
index 000000000..3fd398b53
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Service
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml
new file mode 100644
index 000000000..0c08b6cd2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml
@@ -0,0 +1,5 @@
+exports:
+ defaults:
+ service:
+ type: ClusterIP
+ port: 9090
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml
new file mode 100644
index 000000000..8ed8ddf1f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml
@@ -0,0 +1 @@
+{{- include "library.service" . }}
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml
new file mode 100644
index 000000000..98bd6d24b
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml
@@ -0,0 +1,3 @@
+service:
+ type: ClusterIP
+ port: 8080
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml
new file mode 100644
index 000000000..1a80533d0
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml
@@ -0,0 +1,11 @@
+apiVersion: v3
+name: app3
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+
+dependencies:
+- name: library
+ version: 0.1.0
+ import-values:
+ - defaults
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml
new file mode 100644
index 000000000..9bc306361
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: library
+description: A Helm chart for Kubernetes
+type: library
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml
new file mode 100644
index 000000000..3fd398b53
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Service
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml
new file mode 100644
index 000000000..0c08b6cd2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml
@@ -0,0 +1,5 @@
+exports:
+ defaults:
+ service:
+ type: ClusterIP
+ port: 9090
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml
new file mode 100644
index 000000000..8ed8ddf1f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml
@@ -0,0 +1 @@
+{{- include "library.service" . }}
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml
new file mode 100644
index 000000000..b738e2a57
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml
@@ -0,0 +1,2 @@
+service:
+ type: ClusterIP
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml
new file mode 100644
index 000000000..886b4b1e4
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml
@@ -0,0 +1,9 @@
+apiVersion: v3
+name: app4
+description: A Helm chart for Kubernetes
+type: application
+version: 0.1.0
+
+dependencies:
+- name: library
+ version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml
new file mode 100644
index 000000000..9bc306361
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v3
+name: library
+description: A Helm chart for Kubernetes
+type: library
+version: 0.1.0
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml
new file mode 100644
index 000000000..3fd398b53
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Service
+spec:
+ type: {{ .Values.service.type }}
+ ports:
+ - port: {{ .Values.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml
new file mode 100644
index 000000000..0c08b6cd2
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml
@@ -0,0 +1,5 @@
+exports:
+ defaults:
+ service:
+ type: ClusterIP
+ port: 9090
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml
new file mode 100644
index 000000000..8ed8ddf1f
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml
@@ -0,0 +1 @@
+{{- include "library.service" . }}
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml
new file mode 100644
index 000000000..3728aa930
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml
@@ -0,0 +1,3 @@
+service:
+ type: ClusterIP
+ port: 1234
diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml
new file mode 100644
index 000000000..de0bafa51
--- /dev/null
+++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml
@@ -0,0 +1,14 @@
+app1:
+ enabled: true
+ service:
+ type: ClusterIP
+ port: 3456
+
+app2:
+ enabled: true
+
+app3:
+ enabled: true
+
+app4:
+ enabled: true
diff --git a/internal/chart/v3/util/validate_name.go b/internal/chart/v3/util/validate_name.go
new file mode 100644
index 000000000..6595e085d
--- /dev/null
+++ b/internal/chart/v3/util/validate_name.go
@@ -0,0 +1,111 @@
+/*
+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 util
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+)
+
+// validName is a regular expression for resource names.
+//
+// According to the Kubernetes help text, the regular expression it uses is:
+//
+// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
+//
+// This follows the above regular expression (but requires a full string match, not partial).
+//
+// The Kubernetes documentation is here, though it is not entirely correct:
+// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
+
+var (
+ // errMissingName indicates that a release (name) was not provided.
+ errMissingName = errors.New("no name provided")
+
+ // errInvalidName indicates that an invalid release name was provided
+ errInvalidName = fmt.Errorf(
+ "invalid release name, must match regex %s and the length must not be longer than 53",
+ validName.String())
+
+ // errInvalidKubernetesName indicates that the name does not meet the Kubernetes
+ // restrictions on metadata names.
+ errInvalidKubernetesName = fmt.Errorf(
+ "invalid metadata name, must match regex %s and the length must not be longer than 253",
+ validName.String())
+)
+
+const (
+ // According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names)
+ // some resource names have a max length of 63 characters while others have a max
+ // length of 253 characters. As we cannot be sure the resources used in a chart, we
+ // therefore need to limit it to 63 chars and reserve 10 chars for additional part to name
+ // of the resource. The reason is that chart maintainers can use release name as part of
+ // the resource name (and some additional chars).
+ maxReleaseNameLen = 53
+ // maxMetadataNameLen is the maximum length Kubernetes allows for any name.
+ maxMetadataNameLen = 253
+)
+
+// ValidateReleaseName performs checks for an entry for a Helm release name
+//
+// For Helm to allow a name, it must be below a certain character count (53) and also match
+// a regular expression.
+//
+// According to the Kubernetes help text, the regular expression it uses is:
+//
+// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
+//
+// This follows the above regular expression (but requires a full string match, not partial).
+//
+// The Kubernetes documentation is here, though it is not entirely correct:
+// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+func ValidateReleaseName(name string) error {
+ // This case is preserved for backwards compatibility
+ if name == "" {
+ return errMissingName
+
+ }
+ if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
+ return errInvalidName
+ }
+ return nil
+}
+
+// ValidateMetadataName validates the name field of a Kubernetes metadata object.
+//
+// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
+// will fail.
+//
+// According to the Kubernetes help text, the regular expression it uses is:
+//
+// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
+//
+// This follows the above regular expression (but requires a full string match, not partial).
+//
+// The Kubernetes documentation is here, though it is not entirely correct:
+// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+//
+// Deprecated: remove in Helm 4. Name validation now uses rules defined in
+// pkg/lint/rules.validateMetadataNameFunc()
+func ValidateMetadataName(name string) error {
+ if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
+ return errInvalidKubernetesName
+ }
+ return nil
+}
diff --git a/internal/chart/v3/util/validate_name_test.go b/internal/chart/v3/util/validate_name_test.go
new file mode 100644
index 000000000..cfc62a0f7
--- /dev/null
+++ b/internal/chart/v3/util/validate_name_test.go
@@ -0,0 +1,91 @@
+/*
+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 util
+
+import "testing"
+
+// TestValidateReleaseName is a regression test for ValidateName
+//
+// Kubernetes has strict naming conventions for resource names. This test represents
+// those conventions.
+//
+// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+//
+// NOTE: At the time of this writing, the docs above say that names cannot begin with
+// digits. However, `kubectl`'s regular expression explicit allows this, and
+// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
+func TestValidateReleaseName(t *testing.T) {
+ names := map[string]bool{
+ "": false,
+ "foo": true,
+ "foo.bar1234baz.seventyone": true,
+ "FOO": false,
+ "123baz": true,
+ "foo.BAR.baz": false,
+ "one-two": true,
+ "-two": false,
+ "one_two": false,
+ "a..b": false,
+ "%^$%*@^*@^": false,
+ "example:com": false,
+ "example%%com": false,
+ "a1111111111111111111111111111111111111111111111111111111111z": false,
+ }
+ for input, expectPass := range names {
+ if err := ValidateReleaseName(input); (err == nil) != expectPass {
+ st := "fail"
+ if expectPass {
+ st = "succeed"
+ }
+ t.Errorf("Expected %q to %s", input, st)
+ }
+ }
+}
+
+func TestValidateMetadataName(t *testing.T) {
+ names := map[string]bool{
+ "": false,
+ "foo": true,
+ "foo.bar1234baz.seventyone": true,
+ "FOO": false,
+ "123baz": true,
+ "foo.BAR.baz": false,
+ "one-two": true,
+ "-two": false,
+ "one_two": false,
+ "a..b": false,
+ "%^$%*@^*@^": false,
+ "example:com": false,
+ "example%%com": false,
+ "a1111111111111111111111111111111111111111111111111111111111z": true,
+ "a1111111111111111111111111111111111111111111111111111111111z" +
+ "a1111111111111111111111111111111111111111111111111111111111z" +
+ "a1111111111111111111111111111111111111111111111111111111111z" +
+ "a1111111111111111111111111111111111111111111111111111111111z" +
+ "a1111111111111111111111111111111111111111111111111111111111z" +
+ "a1111111111111111111111111111111111111111111111111111111111z": false,
+ }
+ for input, expectPass := range names {
+ if err := ValidateMetadataName(input); (err == nil) != expectPass {
+ st := "fail"
+ if expectPass {
+ st = "succeed"
+ }
+ t.Errorf("Expected %q to %s", input, st)
+ }
+ }
+}
diff --git a/internal/chart/v3/util/values.go b/internal/chart/v3/util/values.go
new file mode 100644
index 000000000..8e1a14b45
--- /dev/null
+++ b/internal/chart/v3/util/values.go
@@ -0,0 +1,220 @@
+/*
+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 util
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "sigs.k8s.io/yaml"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+// GlobalKey is the name of the Values key that is used for storing global vars.
+const GlobalKey = "global"
+
+// Values represents a collection of chart values.
+type Values map[string]interface{}
+
+// YAML encodes the Values into a YAML string.
+func (v Values) YAML() (string, error) {
+ b, err := yaml.Marshal(v)
+ return string(b), err
+}
+
+// Table gets a table (YAML subsection) from a Values object.
+//
+// The table is returned as a Values.
+//
+// Compound table names may be specified with dots:
+//
+// foo.bar
+//
+// The above will be evaluated as "The table bar inside the table
+// foo".
+//
+// An ErrNoTable is returned if the table does not exist.
+func (v Values) Table(name string) (Values, error) {
+ table := v
+ var err error
+
+ for _, n := range parsePath(name) {
+ if table, err = tableLookup(table, n); err != nil {
+ break
+ }
+ }
+ return table, err
+}
+
+// AsMap is a utility function for converting Values to a map[string]interface{}.
+//
+// It protects against nil map panics.
+func (v Values) AsMap() map[string]interface{} {
+ if len(v) == 0 {
+ return map[string]interface{}{}
+ }
+ return v
+}
+
+// Encode writes serialized Values information to the given io.Writer.
+func (v Values) Encode(w io.Writer) error {
+ out, err := yaml.Marshal(v)
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(out)
+ return err
+}
+
+func tableLookup(v Values, simple string) (Values, error) {
+ v2, ok := v[simple]
+ if !ok {
+ return v, ErrNoTable{simple}
+ }
+ if vv, ok := v2.(map[string]interface{}); ok {
+ return vv, nil
+ }
+
+ // This catches a case where a value is of type Values, but doesn't (for some
+ // reason) match the map[string]interface{}. This has been observed in the
+ // wild, and might be a result of a nil map of type Values.
+ if vv, ok := v2.(Values); ok {
+ return vv, nil
+ }
+
+ return Values{}, ErrNoTable{simple}
+}
+
+// ReadValues will parse YAML byte data into a Values.
+func ReadValues(data []byte) (vals Values, err error) {
+ err = yaml.Unmarshal(data, &vals)
+ if len(vals) == 0 {
+ vals = Values{}
+ }
+ return vals, err
+}
+
+// ReadValuesFile will parse a YAML file into a map of values.
+func ReadValuesFile(filename string) (Values, error) {
+ data, err := os.ReadFile(filename)
+ if err != nil {
+ return map[string]interface{}{}, err
+ }
+ return ReadValues(data)
+}
+
+// ReleaseOptions represents the additional release options needed
+// for the composition of the final values struct
+type ReleaseOptions struct {
+ Name string
+ Namespace string
+ Revision int
+ IsUpgrade bool
+ IsInstall bool
+}
+
+// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
+//
+// This takes both ReleaseOptions and Capabilities to merge into the render values.
+func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) {
+ return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false)
+}
+
+// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files
+//
+// This takes both ReleaseOptions and Capabilities to merge into the render values.
+func ToRenderValuesWithSchemaValidation(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (Values, error) {
+ if caps == nil {
+ caps = DefaultCapabilities
+ }
+ top := map[string]interface{}{
+ "Chart": chrt.Metadata,
+ "Capabilities": caps,
+ "Release": map[string]interface{}{
+ "Name": options.Name,
+ "Namespace": options.Namespace,
+ "IsUpgrade": options.IsUpgrade,
+ "IsInstall": options.IsInstall,
+ "Revision": options.Revision,
+ "Service": "Helm",
+ },
+ }
+
+ vals, err := CoalesceValues(chrt, chrtVals)
+ if err != nil {
+ return top, err
+ }
+
+ if !skipSchemaValidation {
+ if err := ValidateAgainstSchema(chrt, vals); err != nil {
+ return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err)
+ }
+ }
+
+ top["Values"] = vals
+ return top, nil
+}
+
+// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
+func istable(v interface{}) bool {
+ _, ok := v.(map[string]interface{})
+ return ok
+}
+
+// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
+// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
+// Given the following YAML data the value at path "chapter.one.title" is "Loomings".
+//
+// chapter:
+// one:
+// title: "Loomings"
+func (v Values) PathValue(path string) (interface{}, error) {
+ if path == "" {
+ return nil, errors.New("YAML path cannot be empty")
+ }
+ return v.pathValue(parsePath(path))
+}
+
+func (v Values) pathValue(path []string) (interface{}, error) {
+ if len(path) == 1 {
+ // if exists must be root key not table
+ if _, ok := v[path[0]]; ok && !istable(v[path[0]]) {
+ return v[path[0]], nil
+ }
+ return nil, ErrNoValue{path[0]}
+ }
+
+ key, path := path[len(path)-1], path[:len(path)-1]
+ // get our table for table path
+ t, err := v.Table(joinPath(path...))
+ if err != nil {
+ return nil, ErrNoValue{key}
+ }
+ // check table for key and ensure value is not a table
+ if k, ok := t[key]; ok && !istable(k) {
+ return k, nil
+ }
+ return nil, ErrNoValue{key}
+}
+
+func parsePath(key string) []string { return strings.Split(key, ".") }
+
+func joinPath(path ...string) string { return strings.Join(path, ".") }
diff --git a/internal/chart/v3/util/values_test.go b/internal/chart/v3/util/values_test.go
new file mode 100644
index 000000000..34c664581
--- /dev/null
+++ b/internal/chart/v3/util/values_test.go
@@ -0,0 +1,293 @@
+/*
+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 util
+
+import (
+ "bytes"
+ "fmt"
+ "testing"
+ "text/template"
+
+ chart "helm.sh/helm/v4/internal/chart/v3"
+)
+
+func TestReadValues(t *testing.T) {
+ doc := `# Test YAML parse
+poet: "Coleridge"
+title: "Rime of the Ancient Mariner"
+stanza:
+ - "at"
+ - "length"
+ - "did"
+ - cross
+ - an
+ - Albatross
+
+mariner:
+ with: "crossbow"
+ shot: "ALBATROSS"
+
+water:
+ water:
+ where: "everywhere"
+ nor: "any drop to drink"
+`
+
+ data, err := ReadValues([]byte(doc))
+ if err != nil {
+ t.Fatalf("Error parsing bytes: %s", err)
+ }
+ matchValues(t, data)
+
+ tests := []string{`poet: "Coleridge"`, "# Just a comment", ""}
+
+ for _, tt := range tests {
+ data, err = ReadValues([]byte(tt))
+ if err != nil {
+ t.Fatalf("Error parsing bytes (%s): %s", tt, err)
+ }
+ if data == nil {
+ t.Errorf(`YAML string "%s" gave a nil map`, tt)
+ }
+ }
+}
+
+func TestToRenderValues(t *testing.T) {
+
+ chartValues := map[string]interface{}{
+ "name": "al Rashid",
+ "where": map[string]interface{}{
+ "city": "Basrah",
+ "title": "caliph",
+ },
+ }
+
+ overrideValues := map[string]interface{}{
+ "name": "Haroun",
+ "where": map[string]interface{}{
+ "city": "Baghdad",
+ "date": "809 CE",
+ },
+ }
+
+ c := &chart.Chart{
+ Metadata: &chart.Metadata{Name: "test"},
+ Templates: []*chart.File{},
+ Values: chartValues,
+ Files: []*chart.File{
+ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")},
+ },
+ }
+ c.AddDependency(&chart.Chart{
+ Metadata: &chart.Metadata{Name: "where"},
+ })
+
+ o := ReleaseOptions{
+ Name: "Seven Voyages",
+ Namespace: "default",
+ Revision: 1,
+ IsInstall: true,
+ }
+
+ res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Ensure that the top-level values are all set.
+ if name := res["Chart"].(*chart.Metadata).Name; name != "test" {
+ t.Errorf("Expected chart name 'test', got %q", name)
+ }
+ relmap := res["Release"].(map[string]interface{})
+ if name := relmap["Name"]; name.(string) != "Seven Voyages" {
+ t.Errorf("Expected release name 'Seven Voyages', got %q", name)
+ }
+ if namespace := relmap["Namespace"]; namespace.(string) != "default" {
+ t.Errorf("Expected namespace 'default', got %q", namespace)
+ }
+ if revision := relmap["Revision"]; revision.(int) != 1 {
+ t.Errorf("Expected revision '1', got %d", revision)
+ }
+ if relmap["IsUpgrade"].(bool) {
+ t.Error("Expected upgrade to be false.")
+ }
+ if !relmap["IsInstall"].(bool) {
+ t.Errorf("Expected install to be true.")
+ }
+ if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
+ t.Error("Expected Capabilities to have v1 as an API")
+ }
+ if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" {
+ t.Error("Expected Capabilities to have a Kube version")
+ }
+
+ vals := res["Values"].(Values)
+ if vals["name"] != "Haroun" {
+ t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals)
+ }
+ where := vals["where"].(map[string]interface{})
+ expects := map[string]string{
+ "city": "Baghdad",
+ "date": "809 CE",
+ "title": "caliph",
+ }
+ for field, expect := range expects {
+ if got := where[field]; got != expect {
+ t.Errorf("Expected %q, got %q (%v)", expect, got, where)
+ }
+ }
+}
+
+func TestReadValuesFile(t *testing.T) {
+ data, err := ReadValuesFile("./testdata/coleridge.yaml")
+ if err != nil {
+ t.Fatalf("Error reading YAML file: %s", err)
+ }
+ matchValues(t, data)
+}
+
+func ExampleValues() {
+ doc := `
+title: "Moby Dick"
+chapter:
+ one:
+ title: "Loomings"
+ two:
+ title: "The Carpet-Bag"
+ three:
+ title: "The Spouter Inn"
+`
+ d, err := ReadValues([]byte(doc))
+ if err != nil {
+ panic(err)
+ }
+ ch1, err := d.Table("chapter.one")
+ if err != nil {
+ panic("could not find chapter one")
+ }
+ fmt.Print(ch1["title"])
+ // Output:
+ // Loomings
+}
+
+func TestTable(t *testing.T) {
+ doc := `
+title: "Moby Dick"
+chapter:
+ one:
+ title: "Loomings"
+ two:
+ title: "The Carpet-Bag"
+ three:
+ title: "The Spouter Inn"
+`
+ d, err := ReadValues([]byte(doc))
+ if err != nil {
+ t.Fatalf("Failed to parse the White Whale: %s", err)
+ }
+
+ if _, err := d.Table("title"); err == nil {
+ t.Fatalf("Title is not a table.")
+ }
+
+ if _, err := d.Table("chapter"); err != nil {
+ t.Fatalf("Failed to get the chapter table: %s\n%v", err, d)
+ }
+
+ if v, err := d.Table("chapter.one"); err != nil {
+ t.Errorf("Failed to get chapter.one: %s", err)
+ } else if v["title"] != "Loomings" {
+ t.Errorf("Unexpected title: %s", v["title"])
+ }
+
+ if _, err := d.Table("chapter.three"); err != nil {
+ t.Errorf("Chapter three is missing: %s\n%v", err, d)
+ }
+
+ if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil {
+ t.Errorf("I think you mean 'Epilogue'")
+ }
+}
+
+func matchValues(t *testing.T, data map[string]interface{}) {
+ t.Helper()
+ if data["poet"] != "Coleridge" {
+ t.Errorf("Unexpected poet: %s", data["poet"])
+ }
+
+ if o, err := ttpl("{{len .stanza}}", data); err != nil {
+ t.Errorf("len stanza: %s", err)
+ } else if o != "6" {
+ t.Errorf("Expected 6, got %s", o)
+ }
+
+ if o, err := ttpl("{{.mariner.shot}}", data); err != nil {
+ t.Errorf(".mariner.shot: %s", err)
+ } else if o != "ALBATROSS" {
+ t.Errorf("Expected that mariner shot ALBATROSS")
+ }
+
+ if o, err := ttpl("{{.water.water.where}}", data); err != nil {
+ t.Errorf(".water.water.where: %s", err)
+ } else if o != "everywhere" {
+ t.Errorf("Expected water water everywhere")
+ }
+}
+
+func ttpl(tpl string, v map[string]interface{}) (string, error) {
+ var b bytes.Buffer
+ tt := template.Must(template.New("t").Parse(tpl))
+ err := tt.Execute(&b, v)
+ return b.String(), err
+}
+
+func TestPathValue(t *testing.T) {
+ doc := `
+title: "Moby Dick"
+chapter:
+ one:
+ title: "Loomings"
+ two:
+ title: "The Carpet-Bag"
+ three:
+ title: "The Spouter Inn"
+`
+ d, err := ReadValues([]byte(doc))
+ if err != nil {
+ t.Fatalf("Failed to parse the White Whale: %s", err)
+ }
+
+ if v, err := d.PathValue("chapter.one.title"); err != nil {
+ t.Errorf("Got error instead of title: %s\n%v", err, d)
+ } else if v != "Loomings" {
+ t.Errorf("No error but got wrong value for title: %s\n%v", err, d)
+ }
+ if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil {
+ t.Errorf("Non-existent key should return error: %s\n%v", err, d)
+ }
+ if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil {
+ t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d)
+ }
+ if _, err := d.PathValue(""); err == nil {
+ t.Error("Asking for the value from an empty path should yield an error")
+ }
+ if v, err := d.PathValue("title"); err == nil {
+ if v != "Moby Dick" {
+ t.Errorf("Failed to return values for root key title")
+ }
+ }
+}