github.com/koderover/helm@v2.17.0+incompatible/pkg/chartutil/load.go (about)

     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 chartutil
    18  
    19  import (
    20  	"archive/tar"
    21  	"bytes"
    22  	"compress/gzip"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"os"
    29  	"path"
    30  	"path/filepath"
    31  	"regexp"
    32  	"strings"
    33  
    34  	"github.com/golang/protobuf/ptypes/any"
    35  
    36  	"k8s.io/helm/pkg/ignore"
    37  	"k8s.io/helm/pkg/proto/hapi/chart"
    38  	"k8s.io/helm/pkg/sympath"
    39  )
    40  
    41  // Load takes a string name, tries to resolve it to a file or directory, and then loads it.
    42  //
    43  // This is the preferred way to load a chart. It will discover the chart encoding
    44  // and hand off to the appropriate chart reader.
    45  //
    46  // If a .helmignore file is present, the directory loader will skip loading any files
    47  // matching it. But .helmignore is not evaluated when reading out of an archive.
    48  func Load(name string) (*chart.Chart, error) {
    49  	name = filepath.FromSlash(name)
    50  	fi, err := os.Stat(name)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	if fi.IsDir() {
    55  		if validChart, err := IsChartDir(name); !validChart {
    56  			return nil, err
    57  		}
    58  		return LoadDir(name)
    59  	}
    60  	return LoadFile(name)
    61  }
    62  
    63  // BufferedFile represents an archive file buffered for later processing.
    64  type BufferedFile struct {
    65  	Name string
    66  	Data []byte
    67  }
    68  
    69  var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
    70  
    71  // loadArchiveFiles loads files out of an archive
    72  func loadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
    73  	unzipped, err := gzip.NewReader(in)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	defer unzipped.Close()
    78  
    79  	files := []*BufferedFile{}
    80  	tr := tar.NewReader(unzipped)
    81  	for {
    82  		b := bytes.NewBuffer(nil)
    83  		hd, err := tr.Next()
    84  		if err == io.EOF {
    85  			break
    86  		}
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  
    91  		if hd.FileInfo().IsDir() {
    92  			// Use this instead of hd.Typeflag because we don't have to do any
    93  			// inference chasing.
    94  			continue
    95  		}
    96  
    97  		switch hd.Typeflag {
    98  		// We don't want to process these extension header files.
    99  		case tar.TypeXGlobalHeader, tar.TypeXHeader:
   100  			continue
   101  		}
   102  
   103  		// Archive could contain \ if generated on Windows
   104  		delimiter := "/"
   105  		if strings.ContainsRune(hd.Name, '\\') {
   106  			delimiter = "\\"
   107  		}
   108  
   109  		parts := strings.Split(hd.Name, delimiter)
   110  		n := strings.Join(parts[1:], delimiter)
   111  
   112  		// Normalize the path to the / delimiter
   113  		n = strings.Replace(n, delimiter, "/", -1)
   114  
   115  		if path.IsAbs(n) {
   116  			return nil, errors.New("chart illegally contains absolute paths")
   117  		}
   118  
   119  		n = path.Clean(n)
   120  		if n == "." {
   121  			// In this case, the original path was relative when it should have been absolute.
   122  			return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name)
   123  		}
   124  		if strings.HasPrefix(n, "..") {
   125  			return nil, errors.New("chart illegally references parent directory")
   126  		}
   127  
   128  		// In some particularly arcane acts of path creativity, it is possible to intermix
   129  		// UNIX and Windows style paths in such a way that you produce a result of the form
   130  		// c:/foo even after all the built-in absolute path checks. So we explicitly check
   131  		// for this condition.
   132  		if drivePathPattern.MatchString(n) {
   133  			return nil, errors.New("chart contains illegally named files")
   134  		}
   135  
   136  		if parts[0] == "Chart.yaml" {
   137  			return nil, errors.New("chart yaml not in base directory")
   138  		}
   139  
   140  		if _, err := io.Copy(b, tr); err != nil {
   141  			return files, err
   142  		}
   143  
   144  		files = append(files, &BufferedFile{Name: n, Data: b.Bytes()})
   145  		b.Reset()
   146  	}
   147  
   148  	if len(files) == 0 {
   149  		return nil, errors.New("no files in chart archive")
   150  	}
   151  	return files, nil
   152  }
   153  
   154  // LoadArchive loads from a reader containing a compressed tar archive.
   155  func LoadArchive(in io.Reader) (*chart.Chart, error) {
   156  	files, err := loadArchiveFiles(in)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	return LoadFiles(files)
   161  }
   162  
   163  // LoadFiles loads from in-memory files.
   164  func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
   165  	c := &chart.Chart{}
   166  	subcharts := map[string][]*BufferedFile{}
   167  
   168  	for _, f := range files {
   169  		if f.Name == "Chart.yaml" {
   170  			m, err := UnmarshalChartfile(f.Data)
   171  			if err != nil {
   172  				return c, err
   173  			}
   174  			c.Metadata = m
   175  			var apiVersion = c.Metadata.ApiVersion
   176  			if apiVersion != "" && apiVersion != ApiVersionV1 {
   177  				return c, fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v1\"", apiVersion)
   178  			}
   179  		} else if f.Name == "values.toml" {
   180  			return c, errors.New("values.toml is illegal as of 2.0.0-alpha.2")
   181  		} else if f.Name == "values.yaml" {
   182  			c.Values = &chart.Config{Raw: string(f.Data)}
   183  		} else if strings.HasPrefix(f.Name, "templates/") {
   184  			c.Templates = append(c.Templates, &chart.Template{Name: f.Name, Data: f.Data})
   185  		} else if strings.HasPrefix(f.Name, "charts/") {
   186  			if filepath.Ext(f.Name) == ".prov" {
   187  				c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data})
   188  				continue
   189  			}
   190  			cname := strings.TrimPrefix(f.Name, "charts/")
   191  			if strings.IndexAny(cname, "._") == 0 {
   192  				// Ignore charts/ that start with . or _.
   193  				continue
   194  			}
   195  			parts := strings.SplitN(cname, "/", 2)
   196  			scname := parts[0]
   197  			subcharts[scname] = append(subcharts[scname], &BufferedFile{Name: cname, Data: f.Data})
   198  		} else {
   199  			c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data})
   200  		}
   201  	}
   202  
   203  	// Ensure that we got a Chart.yaml file
   204  	if c.Metadata == nil {
   205  		return c, errors.New("chart metadata (Chart.yaml) missing")
   206  	}
   207  	if c.Metadata.Name == "" {
   208  		return c, errors.New("invalid chart (Chart.yaml): name must not be empty")
   209  	}
   210  
   211  	for n, files := range subcharts {
   212  		var sc *chart.Chart
   213  		var err error
   214  		if strings.IndexAny(n, "_.") == 0 {
   215  			continue
   216  		} else if filepath.Ext(n) == ".tgz" {
   217  			file := files[0]
   218  			if file.Name != n {
   219  				return c, fmt.Errorf("error unpacking tar in %s: expected %s, got %s", c.Metadata.Name, n, file.Name)
   220  			}
   221  			// Untar the chart and add to c.Dependencies
   222  			b := bytes.NewBuffer(file.Data)
   223  			sc, err = LoadArchive(b)
   224  		} else {
   225  			// We have to trim the prefix off of every file, and ignore any file
   226  			// that is in charts/, but isn't actually a chart.
   227  			buff := make([]*BufferedFile, 0, len(files))
   228  			for _, f := range files {
   229  				parts := strings.SplitN(f.Name, "/", 2)
   230  				if len(parts) < 2 {
   231  					continue
   232  				}
   233  				f.Name = parts[1]
   234  				buff = append(buff, f)
   235  			}
   236  			sc, err = LoadFiles(buff)
   237  		}
   238  
   239  		if err != nil {
   240  			return c, fmt.Errorf("error unpacking %s in %s: %s", n, c.Metadata.Name, err)
   241  		}
   242  
   243  		c.Dependencies = append(c.Dependencies, sc)
   244  	}
   245  
   246  	return c, nil
   247  }
   248  
   249  // LoadFile loads from an archive file.
   250  func LoadFile(name string) (*chart.Chart, error) {
   251  	if fi, err := os.Stat(name); err != nil {
   252  		return nil, err
   253  	} else if fi.IsDir() {
   254  		return nil, errors.New("cannot load a directory")
   255  	}
   256  
   257  	raw, err := os.Open(name)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  	defer raw.Close()
   262  
   263  	err = ensureArchive(name, raw)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	c, err := LoadArchive(raw)
   269  	if err != nil {
   270  		if err == gzip.ErrHeader {
   271  			return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err)
   272  		}
   273  	}
   274  	return c, err
   275  }
   276  
   277  // ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive.
   278  //
   279  // Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence
   280  // of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error
   281  // if we didn't check for this.
   282  func ensureArchive(name string, raw *os.File) error {
   283  	defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed.
   284  
   285  	// Check the file format to give us a chance to provide the user with more actionable feedback.
   286  	buffer := make([]byte, 512)
   287  	_, err := raw.Read(buffer)
   288  	if err != nil && err != io.EOF {
   289  		return fmt.Errorf("file '%s' cannot be read: %s", name, err)
   290  	}
   291  	if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" {
   292  		// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
   293  		//       variety of content (Makefile, .zshrc) as valid YAML without errors.
   294  
   295  		// Wrong content type. Let's check if it's yaml and give an extra hint?
   296  		if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
   297  			return fmt.Errorf("file '%s' seems to be a YAML file, but I expected a gzipped archive", name)
   298  		}
   299  		return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType)
   300  	}
   301  	return nil
   302  }
   303  
   304  // LoadDir loads from a directory.
   305  //
   306  // This loads charts only from directories.
   307  func LoadDir(dir string) (*chart.Chart, error) {
   308  	topdir, err := filepath.Abs(dir)
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	// Just used for errors.
   314  	c := &chart.Chart{}
   315  
   316  	rules := ignore.Empty()
   317  	ifile := filepath.Join(topdir, ignore.HelmIgnore)
   318  	if _, err := os.Stat(ifile); err == nil {
   319  		r, err := ignore.ParseFile(ifile)
   320  		if err != nil {
   321  			return c, err
   322  		}
   323  		rules = r
   324  	}
   325  	rules.AddDefaults()
   326  
   327  	files := []*BufferedFile{}
   328  	topdir += string(filepath.Separator)
   329  
   330  	walk := func(name string, fi os.FileInfo, err error) error {
   331  		n := strings.TrimPrefix(name, topdir)
   332  		if n == "" {
   333  			// No need to process top level. Avoid bug with helmignore .* matching
   334  			// empty names. See issue 1779.
   335  			return nil
   336  		}
   337  
   338  		// Normalize to / since it will also work on Windows
   339  		n = filepath.ToSlash(n)
   340  
   341  		if err != nil {
   342  			return err
   343  		}
   344  		if fi.IsDir() {
   345  			// Directory-based ignore rules should involve skipping the entire
   346  			// contents of that directory.
   347  			if rules.Ignore(n, fi) {
   348  				return filepath.SkipDir
   349  			}
   350  			return nil
   351  		}
   352  
   353  		// If a .helmignore file matches, skip this file.
   354  		if rules.Ignore(n, fi) {
   355  			return nil
   356  		}
   357  
   358  		// Irregular files include devices, sockets, and other uses of files that
   359  		// are not regular files. In Go they have a file mode type bit set.
   360  		// See https://golang.org/pkg/os/#FileMode for examples.
   361  		if !fi.Mode().IsRegular() {
   362  			return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
   363  		}
   364  
   365  		data, err := ioutil.ReadFile(name)
   366  		if err != nil {
   367  			return fmt.Errorf("error reading %s: %s", n, err)
   368  		}
   369  
   370  		files = append(files, &BufferedFile{Name: n, Data: data})
   371  		return nil
   372  	}
   373  	if err = sympath.Walk(topdir, walk); err != nil {
   374  		return c, err
   375  	}
   376  
   377  	return LoadFiles(files)
   378  }