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