github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+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  	moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz"))
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	archives = append(archives, moreArchives...)
   239  
   240  	index := NewIndexFile()
   241  	for _, arch := range archives {
   242  		fname, err := filepath.Rel(dir, arch)
   243  		if err != nil {
   244  			return index, err
   245  		}
   246  
   247  		var parentDir string
   248  		parentDir, fname = filepath.Split(fname)
   249  		parentURL, err := urlutil.URLJoin(baseURL, parentDir)
   250  		if err != nil {
   251  			parentURL = filepath.Join(baseURL, parentDir)
   252  		}
   253  
   254  		c, err := chartutil.Load(arch)
   255  		if err != nil {
   256  			// Assume this is not a chart.
   257  			continue
   258  		}
   259  		hash, err := provenance.DigestFile(arch)
   260  		if err != nil {
   261  			return index, err
   262  		}
   263  		index.Add(c.Metadata, fname, parentURL, hash)
   264  	}
   265  	return index, nil
   266  }
   267  
   268  // loadIndex loads an index file and does minimal validity checking.
   269  //
   270  // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
   271  func loadIndex(data []byte) (*IndexFile, error) {
   272  	i := &IndexFile{}
   273  	if err := yaml.Unmarshal(data, i); err != nil {
   274  		return i, err
   275  	}
   276  	i.SortEntries()
   277  	if i.APIVersion == "" {
   278  		// When we leave Beta, we should remove legacy support and just
   279  		// return this error:
   280  		//return i, ErrNoAPIVersion
   281  		return loadUnversionedIndex(data)
   282  	}
   283  	return i, nil
   284  }
   285  
   286  // unversionedEntry represents a deprecated pre-Alpha.5 format.
   287  //
   288  // This will be removed prior to v2.0.0
   289  type unversionedEntry struct {
   290  	Checksum  string          `json:"checksum"`
   291  	URL       string          `json:"url"`
   292  	Chartfile *chart.Metadata `json:"chartfile"`
   293  }
   294  
   295  // loadUnversionedIndex loads a pre-Alpha.5 index.yaml file.
   296  //
   297  // This format is deprecated. This function will be removed prior to v2.0.0.
   298  func loadUnversionedIndex(data []byte) (*IndexFile, error) {
   299  	fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'")
   300  	i := map[string]unversionedEntry{}
   301  
   302  	// This gets around an error in the YAML parser. Instead of parsing as YAML,
   303  	// we convert to JSON, and then decode again.
   304  	var err error
   305  	data, err = yaml.YAMLToJSON(data)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	if err := json.Unmarshal(data, &i); err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	if len(i) == 0 {
   314  		return nil, ErrNoAPIVersion
   315  	}
   316  	ni := NewIndexFile()
   317  	for n, item := range i {
   318  		if item.Chartfile == nil || item.Chartfile.Name == "" {
   319  			parts := strings.Split(n, "-")
   320  			ver := ""
   321  			if len(parts) > 1 {
   322  				ver = strings.TrimSuffix(parts[1], ".tgz")
   323  			}
   324  			item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver}
   325  		}
   326  		ni.Add(item.Chartfile, item.URL, "", item.Checksum)
   327  	}
   328  	return ni, nil
   329  }