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