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 }