...

Source file src/helm.sh/helm/v3/pkg/chart/loader/archive.go

Documentation: helm.sh/helm/v3/pkg/chart/loader

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package loader
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"compress/gzip"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"os"
    27  	"path"
    28  	"regexp"
    29  	"strings"
    30  
    31  	"github.com/pkg/errors"
    32  
    33  	"helm.sh/helm/v3/pkg/chart"
    34  )
    35  
    36  var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
    37  
    38  // FileLoader loads a chart from a file
    39  type FileLoader string
    40  
    41  // Load loads a chart
    42  func (l FileLoader) Load() (*chart.Chart, error) {
    43  	return LoadFile(string(l))
    44  }
    45  
    46  // LoadFile loads from an archive file.
    47  func LoadFile(name string) (*chart.Chart, error) {
    48  	if fi, err := os.Stat(name); err != nil {
    49  		return nil, err
    50  	} else if fi.IsDir() {
    51  		return nil, errors.New("cannot load a directory")
    52  	}
    53  
    54  	raw, err := os.Open(name)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	defer raw.Close()
    59  
    60  	err = ensureArchive(name, raw)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	c, err := LoadArchive(raw)
    66  	if err != nil {
    67  		if err == gzip.ErrHeader {
    68  			return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
    69  		}
    70  	}
    71  	return c, err
    72  }
    73  
    74  // ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
    75  //
    76  // Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
    77  // of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
    78  // if we didn't check for this.
    79  func ensureArchive(name string, raw *os.File) error {
    80  	defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
    81  
    82  	// Check the file format to give us a chance to provide the user with more actionable feedback.
    83  	buffer := make([]byte, 512)
    84  	_, err := raw.Read(buffer)
    85  	if err != nil && err != io.EOF {
    86  		return fmt.Errorf("file '%s' cannot be read: %s", name, err)
    87  	}
    88  
    89  	// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
    90  	// Fix for: https://github.com/helm/helm/issues/12261
    91  	if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
    92  		// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
    93  		//       variety of content (Makefile, .zshrc) as valid YAML without errors.
    94  
    95  		// Wrong content type. Let's check if it's yaml and give an extra hint?
    96  		if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
    97  			return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name)
    98  		}
    99  		return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
   100  	}
   101  	return nil
   102  }
   103  
   104  // isGZipApplication checks whether the achieve is of the application/x-gzip type.
   105  func isGZipApplication(data []byte) bool {
   106  	sig := []byte("\x1F\x8B\x08")
   107  	return bytes.HasPrefix(data, sig)
   108  }
   109  
   110  // LoadArchiveFiles reads in files out of an archive into memory. This function
   111  // performs important path security checks and should always be used before
   112  // expanding a tarball
   113  func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
   114  	unzipped, err := gzip.NewReader(in)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	defer unzipped.Close()
   119  
   120  	files := []*BufferedFile{}
   121  	tr := tar.NewReader(unzipped)
   122  	for {
   123  		b := bytes.NewBuffer(nil)
   124  		hd, err := tr.Next()
   125  		if err == io.EOF {
   126  			break
   127  		}
   128  		if err != nil {
   129  			return nil, err
   130  		}
   131  
   132  		if hd.FileInfo().IsDir() {
   133  			// Use this instead of hd.Typeflag because we don't have to do any
   134  			// inference chasing.
   135  			continue
   136  		}
   137  
   138  		switch hd.Typeflag {
   139  		// We don't want to process these extension header files.
   140  		case tar.TypeXGlobalHeader, tar.TypeXHeader:
   141  			continue
   142  		}
   143  
   144  		// Archive could contain \ if generated on Windows
   145  		delimiter := "/"
   146  		if strings.ContainsRune(hd.Name, '\\') {
   147  			delimiter = "\\"
   148  		}
   149  
   150  		parts := strings.Split(hd.Name, delimiter)
   151  		n := strings.Join(parts[1:], delimiter)
   152  
   153  		// Normalize the path to the / delimiter
   154  		n = strings.ReplaceAll(n, delimiter, "/")
   155  
   156  		if path.IsAbs(n) {
   157  			return nil, errors.New("chart illegally contains absolute paths")
   158  		}
   159  
   160  		n = path.Clean(n)
   161  		if n == "." {
   162  			// In this case, the original path was relative when it should have been absolute.
   163  			return nil, errors.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
   164  		}
   165  		if strings.HasPrefix(n, "..") {
   166  			return nil, errors.New("chart illegally references parent directory")
   167  		}
   168  
   169  		// In some particularly arcane acts of path creativity, it is possible to intermix
   170  		// UNIX and Windows style paths in such a way that you produce a result of the form
   171  		// c:/foo even after all the built-in absolute path checks. So we explicitly check
   172  		// for this condition.
   173  		if drivePathPattern.MatchString(n) {
   174  			return nil, errors.New("chart contains illegally named files")
   175  		}
   176  
   177  		if parts[0] == "Chart.yaml" {
   178  			return nil, errors.New("chart yaml not in base directory")
   179  		}
   180  
   181  		if _, err := io.Copy(b, tr); err != nil {
   182  			return nil, err
   183  		}
   184  
   185  		data := bytes.TrimPrefix(b.Bytes(), utf8bom)
   186  
   187  		files = append(files, &BufferedFile{Name: n, Data: data})
   188  		b.Reset()
   189  	}
   190  
   191  	if len(files) == 0 {
   192  		return nil, errors.New("no files in chart archive")
   193  	}
   194  	return files, nil
   195  }
   196  
   197  // LoadArchive loads from a reader containing a compressed tar archive.
   198  func LoadArchive(in io.Reader) (*chart.Chart, error) {
   199  	files, err := LoadArchiveFiles(in)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	return LoadFiles(files)
   205  }
   206  

View as plain text