mirror of https://github.com/helm/helm
This change is about handling the interfaces to public functions for different chart apiVersions. The internals are still focused on v2. This enables v3 to be layered in layer. Signed-off-by: Matt Farina <matt.farina@suse.com>pull/31303/head
parent
1a06fe9901
commit
8dc7c57f50
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
v3chart "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
v2chart "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var NewDependencyAccessor func(dep Dependency) (DependencyAccessor, error) = NewDefaultDependencyAccessor //nolint:revive
|
||||||
|
|
||||||
|
func NewDefaultDependencyAccessor(dep Dependency) (DependencyAccessor, error) {
|
||||||
|
switch v := dep.(type) {
|
||||||
|
case v2chart.Dependency:
|
||||||
|
return &v2DependencyAccessor{&v}, nil
|
||||||
|
case *v2chart.Dependency:
|
||||||
|
return &v2DependencyAccessor{v}, nil
|
||||||
|
case v3chart.Dependency:
|
||||||
|
return &v3DependencyAccessor{&v}, nil
|
||||||
|
case *v3chart.Dependency:
|
||||||
|
return &v3DependencyAccessor{v}, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported chart dependency type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type v2DependencyAccessor struct {
|
||||||
|
dep *v2chart.Dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *v2DependencyAccessor) Name() string {
|
||||||
|
return r.dep.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
type v3DependencyAccessor struct {
|
||||||
|
dep *v3chart.Dependency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *v3DependencyAccessor) Name() string {
|
||||||
|
return r.dep.Name
|
||||||
|
}
|
@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// archive provides utility functions for working with Helm chart archive files
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MaxDecompressedChartSize is the maximum size of a chart archive that will be
|
||||||
|
// decompressed. This is the decompressed size of all the files.
|
||||||
|
// The default value is 100 MiB.
|
||||||
|
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB
|
||||||
|
|
||||||
|
// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load.
|
||||||
|
// The size of the file is the decompressed version of it when it is stored in an archive.
|
||||||
|
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
|
||||||
|
|
||||||
|
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
|
||||||
|
|
||||||
|
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
|
||||||
|
|
||||||
|
// BufferedFile represents an archive file buffered for later processing.
|
||||||
|
type BufferedFile struct {
|
||||||
|
Name string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadArchiveFiles reads in files out of an archive into memory. This function
|
||||||
|
// performs important path security checks and should always be used before
|
||||||
|
// expanding a tarball
|
||||||
|
func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
|
||||||
|
unzipped, err := gzip.NewReader(in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer unzipped.Close()
|
||||||
|
|
||||||
|
files := []*BufferedFile{}
|
||||||
|
tr := tar.NewReader(unzipped)
|
||||||
|
remainingSize := MaxDecompressedChartSize
|
||||||
|
for {
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
hd, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.FileInfo().IsDir() {
|
||||||
|
// Use this instead of hd.Typeflag because we don't have to do any
|
||||||
|
// inference chasing.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch hd.Typeflag {
|
||||||
|
// We don't want to process these extension header files.
|
||||||
|
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive could contain \ if generated on Windows
|
||||||
|
delimiter := "/"
|
||||||
|
if strings.ContainsRune(hd.Name, '\\') {
|
||||||
|
delimiter = "\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(hd.Name, delimiter)
|
||||||
|
n := strings.Join(parts[1:], delimiter)
|
||||||
|
|
||||||
|
// Normalize the path to the / delimiter
|
||||||
|
n = strings.ReplaceAll(n, delimiter, "/")
|
||||||
|
|
||||||
|
if path.IsAbs(n) {
|
||||||
|
return nil, errors.New("chart illegally contains absolute paths")
|
||||||
|
}
|
||||||
|
|
||||||
|
n = path.Clean(n)
|
||||||
|
if n == "." {
|
||||||
|
// In this case, the original path was relative when it should have been absolute.
|
||||||
|
return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(n, "..") {
|
||||||
|
return nil, errors.New("chart illegally references parent directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// In some particularly arcane acts of path creativity, it is possible to intermix
|
||||||
|
// UNIX and Windows style paths in such a way that you produce a result of the form
|
||||||
|
// c:/foo even after all the built-in absolute path checks. So we explicitly check
|
||||||
|
// for this condition.
|
||||||
|
if drivePathPattern.MatchString(n) {
|
||||||
|
return nil, errors.New("chart contains illegally named files")
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[0] == "Chart.yaml" {
|
||||||
|
return nil, errors.New("chart yaml not in base directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Size > remainingSize {
|
||||||
|
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hd.Size > MaxDecompressedFileSize {
|
||||||
|
return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
limitedReader := io.LimitReader(tr, remainingSize)
|
||||||
|
|
||||||
|
bytesWritten, err := io.Copy(b, limitedReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingSize -= bytesWritten
|
||||||
|
// When the bytesWritten are less than the file size it means the limit reader ended
|
||||||
|
// copying early. Here we report that error. This is important if the last file extracted
|
||||||
|
// is the one that goes over the limit. It assumes the Size stored in the tar header
|
||||||
|
// is correct, something many applications do.
|
||||||
|
if bytesWritten < hd.Size || remainingSize <= 0 {
|
||||||
|
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := bytes.TrimPrefix(b.Bytes(), utf8bom)
|
||||||
|
|
||||||
|
files = append(files, &BufferedFile{Name: n, Data: data})
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return nil, errors.New("no files in chart archive")
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
|
||||||
|
//
|
||||||
|
// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
|
||||||
|
// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
|
||||||
|
// if we didn't check for this.
|
||||||
|
func EnsureArchive(name string, raw *os.File) error {
|
||||||
|
defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
|
||||||
|
|
||||||
|
// Check the file format to give us a chance to provide the user with more actionable feedback.
|
||||||
|
buffer := make([]byte, 512)
|
||||||
|
_, err := raw.Read(buffer)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
|
||||||
|
// Fix for: https://github.com/helm/helm/issues/12261
|
||||||
|
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
|
||||||
|
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
|
||||||
|
// variety of content (Makefile, .zshrc) as valid YAML without errors.
|
||||||
|
|
||||||
|
// Wrong content type. Let's check if it's yaml and give an extra hint?
|
||||||
|
if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
||||||
|
return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGZipApplication checks whether the archive is of the application/x-gzip type.
|
||||||
|
func isGZipApplication(data []byte) bool {
|
||||||
|
sig := []byte("\x1F\x8B\x08")
|
||||||
|
return bytes.HasPrefix(data, sig)
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
Copyright The Helm Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
c3 "helm.sh/helm/v4/internal/chart/v3"
|
||||||
|
c3load "helm.sh/helm/v4/internal/chart/v3/loader"
|
||||||
|
"helm.sh/helm/v4/pkg/chart"
|
||||||
|
"helm.sh/helm/v4/pkg/chart/loader/archive"
|
||||||
|
c2 "helm.sh/helm/v4/pkg/chart/v2"
|
||||||
|
c2load "helm.sh/helm/v4/pkg/chart/v2/loader"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChartLoader loads a chart.
|
||||||
|
type ChartLoader interface {
|
||||||
|
Load() (chart.Charter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader returns a new ChartLoader appropriate for the given chart name
|
||||||
|
func Loader(name string) (ChartLoader, error) {
|
||||||
|
fi, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fi.IsDir() {
|
||||||
|
return DirLoader(name), nil
|
||||||
|
}
|
||||||
|
return FileLoader(name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
|
||||||
|
//
|
||||||
|
// This is the preferred way to load a chart. It will discover the chart encoding
|
||||||
|
// and hand off to the appropriate chart reader.
|
||||||
|
//
|
||||||
|
// If a .helmignore file is present, the directory loader will skip loading any files
|
||||||
|
// matching it. But .helmignore is not evaluated when reading out of an archive.
|
||||||
|
func Load(name string) (chart.Charter, error) {
|
||||||
|
l, err := Loader(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return l.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirLoader loads a chart from a directory
|
||||||
|
type DirLoader string
|
||||||
|
|
||||||
|
// Load loads the chart
|
||||||
|
func (l DirLoader) Load() (chart.Charter, error) {
|
||||||
|
return LoadDir(string(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadDir(dir string) (chart.Charter, error) {
|
||||||
|
topdir, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := filepath.Join(topdir, "Chart.yaml")
|
||||||
|
data, err := os.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to detect chart at %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(chartBase)
|
||||||
|
err = yaml.Unmarshal(data, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot load Chart.yaml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.APIVersion {
|
||||||
|
case c2.APIVersionV1, c2.APIVersionV2, "":
|
||||||
|
return c2load.Load(dir)
|
||||||
|
case c3.APIVersionV3:
|
||||||
|
return c3load.Load(dir)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported chart version")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileLoader loads a chart from a file
|
||||||
|
type FileLoader string
|
||||||
|
|
||||||
|
// Load loads a chart
|
||||||
|
func (l FileLoader) Load() (chart.Charter, error) {
|
||||||
|
return LoadFile(string(l))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadFile(name string) (chart.Charter, error) {
|
||||||
|
if fi, err := os.Stat(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if fi.IsDir() {
|
||||||
|
return nil, errors.New("cannot load a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer raw.Close()
|
||||||
|
|
||||||
|
err = archive.EnsureArchive(name, raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := archive.LoadArchiveFiles(raw)
|
||||||
|
if err != nil {
|
||||||
|
if err == gzip.ErrHeader {
|
||||||
|
return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
|
||||||
|
}
|
||||||
|
return nil, errors.New("unable to load chart archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if f.Name == "Chart.yaml" {
|
||||||
|
c := new(chartBase)
|
||||||
|
if err := yaml.Unmarshal(f.Data, c); err != nil {
|
||||||
|
return c, fmt.Errorf("cannot load Chart.yaml: %w", err)
|
||||||
|
}
|
||||||
|
switch c.APIVersion {
|
||||||
|
case c2.APIVersionV1, c2.APIVersionV2, "":
|
||||||
|
return c2load.Load(name)
|
||||||
|
case c3.APIVersionV3:
|
||||||
|
return c3load.Load(name)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported chart version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unable to detect chart version, no Chart.yaml found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// chartBase is used to detect the API Version for the chart to run it through the
|
||||||
|
// loader for that type.
|
||||||
|
type chartBase struct {
|
||||||
|
APIVersion string `json:"apiVersion,omitempty"`
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright The Helm Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package loader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadArchiveFiles(t *testing.T) {
|
|
||||||
tcs := []struct {
|
|
||||||
name string
|
|
||||||
generate func(w *tar.Writer)
|
|
||||||
check func(t *testing.T, files []*BufferedFile, err error)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty input should return no files",
|
|
||||||
generate: func(_ *tar.Writer) {},
|
|
||||||
check: func(t *testing.T, _ []*BufferedFile, err error) {
|
|
||||||
t.Helper()
|
|
||||||
if err.Error() != "no files in chart archive" {
|
|
||||||
t.Fatalf(`expected "no files in chart archive", got [%#v]`, err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "should ignore files with XGlobalHeader type",
|
|
||||||
generate: func(w *tar.Writer) {
|
|
||||||
// simulate the presence of a `pax_global_header` file like you would get when
|
|
||||||
// processing a GitHub release archive.
|
|
||||||
err := w.WriteHeader(&tar.Header{
|
|
||||||
Typeflag: tar.TypeXGlobalHeader,
|
|
||||||
Name: "pax_global_header",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to have at least one file, otherwise we'll get the "no files in chart archive" error
|
|
||||||
err = w.WriteHeader(&tar.Header{
|
|
||||||
Typeflag: tar.TypeReg,
|
|
||||||
Name: "dir/empty",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
check: func(t *testing.T, files []*BufferedFile, err error) {
|
|
||||||
t.Helper()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) != 1 {
|
|
||||||
t.Fatalf(`expected to get one file but got [%v]`, files)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tcs {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
gzw := gzip.NewWriter(buf)
|
|
||||||
tw := tar.NewWriter(gzw)
|
|
||||||
|
|
||||||
tc.generate(tw)
|
|
||||||
|
|
||||||
_ = tw.Close()
|
|
||||||
_ = gzw.Close()
|
|
||||||
|
|
||||||
files, err := LoadArchiveFiles(buf)
|
|
||||||
tc.check(t, files, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue