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