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