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