github.com/felipejfc/helm@v2.1.2+incompatible/pkg/repo/index.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 repo 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "net/url" 26 "os" 27 "path" 28 "path/filepath" 29 "sort" 30 "strings" 31 "time" 32 33 "github.com/Masterminds/semver" 34 "github.com/ghodss/yaml" 35 36 "k8s.io/helm/pkg/chartutil" 37 "k8s.io/helm/pkg/proto/hapi/chart" 38 "k8s.io/helm/pkg/provenance" 39 ) 40 41 var indexPath = "index.yaml" 42 43 // APIVersionV1 is the v1 API version for index and repository files. 44 const APIVersionV1 = "v1" 45 46 var ( 47 // ErrNoAPIVersion indicates that an API version was not specified. 48 ErrNoAPIVersion = errors.New("no API version specified") 49 // ErrNoChartVersion indicates that a chart with the given version is not found. 50 ErrNoChartVersion = errors.New("no chart version found") 51 // ErrNoChartName indicates that a chart with the given name is not found. 52 ErrNoChartName = errors.New("no chart name found") 53 ) 54 55 // ChartVersions is a list of versioned chart references. 56 // Implements a sorter on Version. 57 type ChartVersions []*ChartVersion 58 59 // Len returns the length. 60 func (c ChartVersions) Len() int { return len(c) } 61 62 // Swap swaps the position of two items in the versions slice. 63 func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 64 65 // Less returns true if the version of entry a is less than the version of entry b. 66 func (c ChartVersions) Less(a, b int) bool { 67 // Failed parse pushes to the back. 68 i, err := semver.NewVersion(c[a].Version) 69 if err != nil { 70 return true 71 } 72 j, err := semver.NewVersion(c[b].Version) 73 if err != nil { 74 return false 75 } 76 return i.LessThan(j) 77 } 78 79 // IndexFile represents the index file in a chart repository 80 type IndexFile struct { 81 APIVersion string `json:"apiVersion"` 82 Generated time.Time `json:"generated"` 83 Entries map[string]ChartVersions `json:"entries"` 84 PublicKeys []string `json:"publicKeys,omitempty"` 85 } 86 87 // NewIndexFile initializes an index. 88 func NewIndexFile() *IndexFile { 89 return &IndexFile{ 90 APIVersion: APIVersionV1, 91 Generated: time.Now(), 92 Entries: map[string]ChartVersions{}, 93 PublicKeys: []string{}, 94 } 95 } 96 97 // Add adds a file to the index 98 // This can leave the index in an unsorted state 99 func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { 100 u := filename 101 if baseURL != "" { 102 var err error 103 _, file := filepath.Split(filename) 104 u, err = urlJoin(baseURL, file) 105 if err != nil { 106 u = filepath.Join(baseURL, file) 107 } 108 } 109 cr := &ChartVersion{ 110 URLs: []string{u}, 111 Metadata: md, 112 Digest: digest, 113 Created: time.Now(), 114 } 115 if ee, ok := i.Entries[md.Name]; !ok { 116 i.Entries[md.Name] = ChartVersions{cr} 117 } else { 118 i.Entries[md.Name] = append(ee, cr) 119 } 120 } 121 122 // Has returns true if the index has an entry for a chart with the given name and exact version. 123 func (i IndexFile) Has(name, version string) bool { 124 _, err := i.Get(name, version) 125 return err == nil 126 } 127 128 // SortEntries sorts the entries by version in descending order. 129 // 130 // In canonical form, the individual version records should be sorted so that 131 // the most recent release for every version is in the 0th slot in the 132 // Entries.ChartVersions array. That way, tooling can predict the newest 133 // version without needing to parse SemVers. 134 func (i IndexFile) SortEntries() { 135 for _, versions := range i.Entries { 136 sort.Sort(sort.Reverse(versions)) 137 } 138 } 139 140 // Get returns the ChartVersion for the given name. 141 // 142 // If version is empty, this will return the chart with the highest version. 143 func (i IndexFile) Get(name, version string) (*ChartVersion, error) { 144 vs, ok := i.Entries[name] 145 if !ok { 146 return nil, ErrNoChartName 147 } 148 if len(vs) == 0 { 149 return nil, ErrNoChartVersion 150 } 151 if len(version) == 0 { 152 return vs[0], nil 153 } 154 for _, ver := range vs { 155 // TODO: Do we need to normalize the version field with the SemVer lib? 156 if ver.Version == version { 157 return ver, nil 158 } 159 } 160 return nil, fmt.Errorf("No chart version found for %s-%s", name, version) 161 } 162 163 // WriteFile writes an index file to the given destination path. 164 // 165 // The mode on the file is set to 'mode'. 166 func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { 167 b, err := yaml.Marshal(i) 168 if err != nil { 169 return err 170 } 171 return ioutil.WriteFile(dest, b, mode) 172 } 173 174 // Merge merges the given index file into this index. 175 // 176 // This merges by name and version. 177 // 178 // If one of the entries in the given index does _not_ already exist, it is added. 179 // In all other cases, the existing record is preserved. 180 // 181 // This can leave the index in an unsorted state 182 func (i *IndexFile) Merge(f *IndexFile) { 183 for _, cvs := range f.Entries { 184 for _, cv := range cvs { 185 if !i.Has(cv.Name, cv.Version) { 186 e := i.Entries[cv.Name] 187 i.Entries[cv.Name] = append(e, cv) 188 } 189 } 190 } 191 } 192 193 // Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 194 195 // ChartVersion represents a chart entry in the IndexFile 196 type ChartVersion struct { 197 *chart.Metadata 198 URLs []string `json:"urls"` 199 Created time.Time `json:"created,omitempty"` 200 Removed bool `json:"removed,omitempty"` 201 Digest string `json:"digest,omitempty"` 202 } 203 204 // IndexDirectory reads a (flat) directory and generates an index. 205 // 206 // It indexes only charts that have been packaged (*.tgz). 207 // 208 // The index returned will be in an unsorted state 209 func IndexDirectory(dir, baseURL string) (*IndexFile, error) { 210 archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) 211 if err != nil { 212 return nil, err 213 } 214 index := NewIndexFile() 215 for _, arch := range archives { 216 fname := filepath.Base(arch) 217 c, err := chartutil.Load(arch) 218 if err != nil { 219 // Assume this is not a chart. 220 continue 221 } 222 hash, err := provenance.DigestFile(arch) 223 if err != nil { 224 return index, err 225 } 226 index.Add(c.Metadata, fname, baseURL, hash) 227 } 228 return index, nil 229 } 230 231 // DownloadIndexFile fetches the index from a repository. 232 func DownloadIndexFile(repoName, url, indexFilePath string) error { 233 var indexURL string 234 235 indexURL = strings.TrimSuffix(url, "/") + "/index.yaml" 236 resp, err := http.Get(indexURL) 237 if err != nil { 238 return err 239 } 240 defer resp.Body.Close() 241 242 b, err := ioutil.ReadAll(resp.Body) 243 if err != nil { 244 return err 245 } 246 247 if _, err := LoadIndex(b); err != nil { 248 return err 249 } 250 251 return ioutil.WriteFile(indexFilePath, b, 0644) 252 } 253 254 // LoadIndex loads an index file and does minimal validity checking. 255 // 256 // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. 257 func LoadIndex(data []byte) (*IndexFile, error) { 258 i := &IndexFile{} 259 if err := yaml.Unmarshal(data, i); err != nil { 260 return i, err 261 } 262 if i.APIVersion == "" { 263 // When we leave Beta, we should remove legacy support and just 264 // return this error: 265 //return i, ErrNoAPIVersion 266 return loadUnversionedIndex(data) 267 } 268 return i, nil 269 } 270 271 // unversionedEntry represents a deprecated pre-Alpha.5 format. 272 // 273 // This will be removed prior to v2.0.0 274 type unversionedEntry struct { 275 Checksum string `json:"checksum"` 276 URL string `json:"url"` 277 Chartfile *chart.Metadata `json:"chartfile"` 278 } 279 280 // loadUnversionedIndex loads a pre-Alpha.5 index.yaml file. 281 // 282 // This format is deprecated. This function will be removed prior to v2.0.0. 283 func loadUnversionedIndex(data []byte) (*IndexFile, error) { 284 fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'") 285 i := map[string]unversionedEntry{} 286 287 // This gets around an error in the YAML parser. Instead of parsing as YAML, 288 // we convert to JSON, and then decode again. 289 var err error 290 data, err = yaml.YAMLToJSON(data) 291 if err != nil { 292 return nil, err 293 } 294 if err := json.Unmarshal(data, &i); err != nil { 295 return nil, err 296 } 297 298 if len(i) == 0 { 299 return nil, ErrNoAPIVersion 300 } 301 ni := NewIndexFile() 302 for n, item := range i { 303 if item.Chartfile == nil || item.Chartfile.Name == "" { 304 parts := strings.Split(n, "-") 305 ver := "" 306 if len(parts) > 1 { 307 ver = strings.TrimSuffix(parts[1], ".tgz") 308 } 309 item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver} 310 } 311 ni.Add(item.Chartfile, item.URL, "", item.Checksum) 312 } 313 return ni, nil 314 } 315 316 // LoadIndexFile takes a file at the given path and returns an IndexFile object 317 func LoadIndexFile(path string) (*IndexFile, error) { 318 b, err := ioutil.ReadFile(path) 319 if err != nil { 320 return nil, err 321 } 322 return LoadIndex(b) 323 } 324 325 // urlJoin joins a base URL to one or more path components. 326 // 327 // It's like filepath.Join for URLs. If the baseURL is pathish, this will still 328 // perform a join. 329 // 330 // If the URL is unparsable, this returns an error. 331 func urlJoin(baseURL string, paths ...string) (string, error) { 332 u, err := url.Parse(baseURL) 333 if err != nil { 334 return "", err 335 } 336 // We want path instead of filepath because path always uses /. 337 all := []string{u.Path} 338 all = append(all, paths...) 339 u.Path = path.Join(all...) 340 return u.String(), nil 341 }