github.com/koderover/helm@v2.17.0+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 "errors" 22 "fmt" 23 "io/ioutil" 24 "os" 25 "path" 26 "path/filepath" 27 "sort" 28 "strings" 29 "time" 30 31 "github.com/Masterminds/semver" 32 "github.com/ghodss/yaml" 33 yaml2 "gopkg.in/yaml.v2" 34 35 "k8s.io/helm/pkg/chartutil" 36 "k8s.io/helm/pkg/proto/hapi/chart" 37 "k8s.io/helm/pkg/provenance" 38 "k8s.io/helm/pkg/urlutil" 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 // IndexValidation is used to validate the integrity of an index file 88 type IndexValidation struct { 89 // This is used ONLY for validation against chartmuseum's index files and 90 // is discarded after validation. 91 ServerInfo map[string]interface{} `yaml:"serverInfo,omitempty"` 92 APIVersion string `yaml:"apiVersion"` 93 Generated time.Time `yaml:"generated"` 94 Entries map[string]interface{} `yaml:"entries"` 95 PublicKeys []string `yaml:"publicKeys,omitempty"` 96 } 97 98 // NewIndexFile initializes an index. 99 func NewIndexFile() *IndexFile { 100 return &IndexFile{ 101 APIVersion: APIVersionV1, 102 Generated: time.Now(), 103 Entries: map[string]ChartVersions{}, 104 PublicKeys: []string{}, 105 } 106 } 107 108 // LoadIndexFile takes a file at the given path and returns an IndexFile object 109 func LoadIndexFile(path string) (*IndexFile, error) { 110 b, err := ioutil.ReadFile(path) 111 if err != nil { 112 return nil, err 113 } 114 return loadIndex(b) 115 } 116 117 // Add adds a file to the index 118 // This can leave the index in an unsorted state 119 func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { 120 u := filename 121 if baseURL != "" { 122 var err error 123 _, file := filepath.Split(filename) 124 u, err = urlutil.URLJoin(baseURL, file) 125 if err != nil { 126 u = path.Join(baseURL, file) 127 } 128 } 129 cr := &ChartVersion{ 130 URLs: []string{u}, 131 Metadata: md, 132 Digest: digest, 133 Created: time.Now(), 134 } 135 if ee, ok := i.Entries[md.Name]; !ok { 136 i.Entries[md.Name] = ChartVersions{cr} 137 } else { 138 i.Entries[md.Name] = append(ee, cr) 139 } 140 } 141 142 // Has returns true if the index has an entry for a chart with the given name and exact version. 143 func (i IndexFile) Has(name, version string) bool { 144 _, err := i.Get(name, version) 145 return err == nil 146 } 147 148 // SortEntries sorts the entries by version in descending order. 149 // 150 // In canonical form, the individual version records should be sorted so that 151 // the most recent release for every version is in the 0th slot in the 152 // Entries.ChartVersions array. That way, tooling can predict the newest 153 // version without needing to parse SemVers. 154 func (i IndexFile) SortEntries() { 155 for _, versions := range i.Entries { 156 sort.Sort(sort.Reverse(versions)) 157 } 158 } 159 160 // Get returns the ChartVersion for the given name. 161 // 162 // If version is empty, this will return the chart with the latest stable version, 163 // prerelease versions will be skipped. 164 func (i IndexFile) Get(name, version string) (*ChartVersion, error) { 165 vs, ok := i.Entries[name] 166 if !ok { 167 return nil, ErrNoChartName 168 } 169 if len(vs) == 0 { 170 return nil, ErrNoChartVersion 171 } 172 173 var constraint *semver.Constraints 174 if len(version) == 0 { 175 constraint, _ = semver.NewConstraint("*") 176 } else { 177 var err error 178 constraint, err = semver.NewConstraint(version) 179 if err != nil { 180 return nil, err 181 } 182 } 183 184 // when customer input exact version, check whether have exact match one first 185 if len(version) != 0 { 186 for _, ver := range vs { 187 if version == ver.Version { 188 return ver, nil 189 } 190 } 191 } 192 193 for _, ver := range vs { 194 test, err := semver.NewVersion(ver.Version) 195 if err != nil { 196 continue 197 } 198 199 if constraint.Check(test) { 200 return ver, nil 201 } 202 } 203 return nil, fmt.Errorf("No chart version found for %s-%s", name, version) 204 } 205 206 // WriteFile writes an index file to the given destination path. 207 // 208 // The mode on the file is set to 'mode'. 209 func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { 210 b, err := yaml.Marshal(i) 211 if err != nil { 212 return err 213 } 214 return ioutil.WriteFile(dest, b, mode) 215 } 216 217 // Merge merges the given index file into this index. 218 // 219 // This merges by name and version. 220 // 221 // If one of the entries in the given index does _not_ already exist, it is added. 222 // In all other cases, the existing record is preserved. 223 // 224 // This can leave the index in an unsorted state 225 func (i *IndexFile) Merge(f *IndexFile) { 226 for _, cvs := range f.Entries { 227 for _, cv := range cvs { 228 if !i.Has(cv.Name, cv.Version) { 229 e := i.Entries[cv.Name] 230 i.Entries[cv.Name] = append(e, cv) 231 } 232 } 233 } 234 } 235 236 // Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 237 238 // ChartVersion represents a chart entry in the IndexFile 239 type ChartVersion struct { 240 *chart.Metadata 241 URLs []string `json:"urls"` 242 Created time.Time `json:"created,omitempty"` 243 Removed bool `json:"removed,omitempty"` 244 Digest string `json:"digest,omitempty"` 245 } 246 247 // IndexDirectory reads a (flat) directory and generates an index. 248 // 249 // It indexes only charts that have been packaged (*.tgz). 250 // 251 // The index returned will be in an unsorted state 252 func IndexDirectory(dir, baseURL string) (*IndexFile, error) { 253 archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) 254 if err != nil { 255 return nil, err 256 } 257 moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz")) 258 if err != nil { 259 return nil, err 260 } 261 archives = append(archives, moreArchives...) 262 263 index := NewIndexFile() 264 for _, arch := range archives { 265 fname, err := filepath.Rel(dir, arch) 266 if err != nil { 267 return index, err 268 } 269 270 var parentDir string 271 parentDir, fname = filepath.Split(fname) 272 // filepath.Split appends an extra slash to the end of parentDir. We want to strip that out. 273 parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator)) 274 parentURL, err := urlutil.URLJoin(baseURL, parentDir) 275 if err != nil { 276 parentURL = path.Join(baseURL, parentDir) 277 } 278 279 c, err := chartutil.Load(arch) 280 if err != nil { 281 // Assume this is not a chart. 282 continue 283 } 284 hash, err := provenance.DigestFile(arch) 285 if err != nil { 286 return index, err 287 } 288 index.Add(c.Metadata, fname, parentURL, hash) 289 } 290 return index, nil 291 } 292 293 // loadIndex loads an index file and does minimal validity checking. 294 // 295 // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. 296 func loadIndex(data []byte) (*IndexFile, error) { 297 i := &IndexFile{} 298 if err := validateIndex(data); err != nil { 299 return i, err 300 } 301 302 if err := yaml.Unmarshal(data, i); err != nil { 303 return i, err 304 } 305 306 i.SortEntries() 307 if i.APIVersion == "" { 308 // When we leave Beta, we should remove legacy support and just 309 // return this error: 310 //return i, ErrNoAPIVersion 311 return loadUnversionedIndex(data) 312 } 313 return i, nil 314 } 315 316 // validateIndex validates that the index is well-formed. 317 func validateIndex(data []byte) error { 318 // This is done ONLY for validation. We need to use ghodss/yaml for the actual parsing. 319 validation := &IndexValidation{} 320 if err := yaml2.UnmarshalStrict(data, validation); err != nil { 321 return err 322 } 323 return nil 324 } 325 326 // unversionedEntry represents a deprecated pre-Alpha.5 format. 327 // 328 // This will be removed prior to v2.0.0 329 type unversionedEntry struct { 330 Checksum string `json:"checksum"` 331 URL string `json:"url"` 332 Chartfile *chart.Metadata `json:"chartfile"` 333 } 334 335 // loadUnversionedIndex loads a pre-Alpha.5 index.yaml file. 336 // 337 // This format is deprecated. This function will be removed prior to v2.0.0. 338 func loadUnversionedIndex(data []byte) (*IndexFile, error) { 339 fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'") 340 i := map[string]unversionedEntry{} 341 342 // This gets around an error in the YAML parser. Instead of parsing as YAML, 343 // we convert to JSON, and then decode again. 344 var err error 345 data, err = yaml.YAMLToJSON(data) 346 if err != nil { 347 return nil, err 348 } 349 if err := json.Unmarshal(data, &i); err != nil { 350 return nil, err 351 } 352 353 if len(i) == 0 { 354 return nil, ErrNoAPIVersion 355 } 356 ni := NewIndexFile() 357 for n, item := range i { 358 if item.Chartfile == nil || item.Chartfile.Name == "" { 359 parts := strings.Split(n, "-") 360 ver := "" 361 if len(parts) > 1 { 362 ver = strings.TrimSuffix(parts[1], ".tgz") 363 } 364 item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver} 365 } 366 ni.Add(item.Chartfile, item.URL, "", item.Checksum) 367 } 368 return ni, nil 369 }