github.com/cloudposse/helm@v2.2.3+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  	if len(version) == 0 {
   159  		return vs[0], nil
   160  	}
   161  	for _, ver := range vs {
   162  		// TODO: Do we need to normalize the version field with the SemVer lib?
   163  		if ver.Version == version {
   164  			return ver, nil
   165  		}
   166  	}
   167  	return nil, fmt.Errorf("No chart version found for %s-%s", name, version)
   168  }
   169  
   170  // WriteFile writes an index file to the given destination path.
   171  //
   172  // The mode on the file is set to 'mode'.
   173  func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
   174  	b, err := yaml.Marshal(i)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	return ioutil.WriteFile(dest, b, mode)
   179  }
   180  
   181  // Merge merges the given index file into this index.
   182  //
   183  // This merges by name and version.
   184  //
   185  // If one of the entries in the given index does _not_ already exist, it is added.
   186  // In all other cases, the existing record is preserved.
   187  //
   188  // This can leave the index in an unsorted state
   189  func (i *IndexFile) Merge(f *IndexFile) {
   190  	for _, cvs := range f.Entries {
   191  		for _, cv := range cvs {
   192  			if !i.Has(cv.Name, cv.Version) {
   193  				e := i.Entries[cv.Name]
   194  				i.Entries[cv.Name] = append(e, cv)
   195  			}
   196  		}
   197  	}
   198  }
   199  
   200  // Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2
   201  
   202  // ChartVersion represents a chart entry in the IndexFile
   203  type ChartVersion struct {
   204  	*chart.Metadata
   205  	URLs    []string  `json:"urls"`
   206  	Created time.Time `json:"created,omitempty"`
   207  	Removed bool      `json:"removed,omitempty"`
   208  	Digest  string    `json:"digest,omitempty"`
   209  }
   210  
   211  // IndexDirectory reads a (flat) directory and generates an index.
   212  //
   213  // It indexes only charts that have been packaged (*.tgz).
   214  //
   215  // The index returned will be in an unsorted state
   216  func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
   217  	archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	index := NewIndexFile()
   222  	for _, arch := range archives {
   223  		fname := filepath.Base(arch)
   224  		c, err := chartutil.Load(arch)
   225  		if err != nil {
   226  			// Assume this is not a chart.
   227  			continue
   228  		}
   229  		hash, err := provenance.DigestFile(arch)
   230  		if err != nil {
   231  			return index, err
   232  		}
   233  		index.Add(c.Metadata, fname, baseURL, hash)
   234  	}
   235  	return index, nil
   236  }
   237  
   238  // loadIndex loads an index file and does minimal validity checking.
   239  //
   240  // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
   241  func loadIndex(data []byte) (*IndexFile, error) {
   242  	i := &IndexFile{}
   243  	if err := yaml.Unmarshal(data, i); err != nil {
   244  		return i, err
   245  	}
   246  	if i.APIVersion == "" {
   247  		// When we leave Beta, we should remove legacy support and just
   248  		// return this error:
   249  		//return i, ErrNoAPIVersion
   250  		return loadUnversionedIndex(data)
   251  	}
   252  	return i, nil
   253  }
   254  
   255  // unversionedEntry represents a deprecated pre-Alpha.5 format.
   256  //
   257  // This will be removed prior to v2.0.0
   258  type unversionedEntry struct {
   259  	Checksum  string          `json:"checksum"`
   260  	URL       string          `json:"url"`
   261  	Chartfile *chart.Metadata `json:"chartfile"`
   262  }
   263  
   264  // loadUnversionedIndex loads a pre-Alpha.5 index.yaml file.
   265  //
   266  // This format is deprecated. This function will be removed prior to v2.0.0.
   267  func loadUnversionedIndex(data []byte) (*IndexFile, error) {
   268  	fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'")
   269  	i := map[string]unversionedEntry{}
   270  
   271  	// This gets around an error in the YAML parser. Instead of parsing as YAML,
   272  	// we convert to JSON, and then decode again.
   273  	var err error
   274  	data, err = yaml.YAMLToJSON(data)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	if err := json.Unmarshal(data, &i); err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	if len(i) == 0 {
   283  		return nil, ErrNoAPIVersion
   284  	}
   285  	ni := NewIndexFile()
   286  	for n, item := range i {
   287  		if item.Chartfile == nil || item.Chartfile.Name == "" {
   288  			parts := strings.Split(n, "-")
   289  			ver := ""
   290  			if len(parts) > 1 {
   291  				ver = strings.TrimSuffix(parts[1], ".tgz")
   292  			}
   293  			item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver}
   294  		}
   295  		ni.Add(item.Chartfile, item.URL, "", item.Checksum)
   296  	}
   297  	return ni, nil
   298  }