github.com/doitroot/helm@v3.0.0-beta.3+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  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/Masterminds/semver"
    31  	"github.com/pkg/errors"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"helm.sh/helm/internal/urlutil"
    35  	"helm.sh/helm/pkg/chart"
    36  	"helm.sh/helm/pkg/chart/loader"
    37  	"helm.sh/helm/pkg/provenance"
    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 highest version.
   151  func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
   152  	vs, ok := i.Entries[name]
   153  	if !ok {
   154  		return nil, ErrNoChartName
   155  	}
   156  	if len(vs) == 0 {
   157  		return nil, ErrNoChartVersion
   158  	}
   159  
   160  	var constraint *semver.Constraints
   161  	if version == "" {
   162  		constraint, _ = semver.NewConstraint("*")
   163  	} else {
   164  		var err error
   165  		constraint, err = semver.NewConstraint(version)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	for _, ver := range vs {
   172  		test, err := semver.NewVersion(ver.Version)
   173  		if err != nil {
   174  			continue
   175  		}
   176  
   177  		if constraint.Check(test) {
   178  			return ver, nil
   179  		}
   180  	}
   181  	return nil, errors.Errorf("no chart version found for %s-%s", name, version)
   182  }
   183  
   184  // WriteFile writes an index file to the given destination path.
   185  //
   186  // The mode on the file is set to 'mode'.
   187  func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
   188  	b, err := yaml.Marshal(i)
   189  	if err != nil {
   190  		return err
   191  	}
   192  	return ioutil.WriteFile(dest, b, mode)
   193  }
   194  
   195  // Merge merges the given index file into this index.
   196  //
   197  // This merges by name and version.
   198  //
   199  // If one of the entries in the given index does _not_ already exist, it is added.
   200  // In all other cases, the existing record is preserved.
   201  //
   202  // This can leave the index in an unsorted state
   203  func (i *IndexFile) Merge(f *IndexFile) {
   204  	for _, cvs := range f.Entries {
   205  		for _, cv := range cvs {
   206  			if !i.Has(cv.Name, cv.Version) {
   207  				e := i.Entries[cv.Name]
   208  				i.Entries[cv.Name] = append(e, cv)
   209  			}
   210  		}
   211  	}
   212  }
   213  
   214  // ChartVersion represents a chart entry in the IndexFile
   215  type ChartVersion struct {
   216  	*chart.Metadata
   217  	URLs    []string  `json:"urls"`
   218  	Created time.Time `json:"created,omitempty"`
   219  	Removed bool      `json:"removed,omitempty"`
   220  	Digest  string    `json:"digest,omitempty"`
   221  }
   222  
   223  // IndexDirectory reads a (flat) directory and generates an index.
   224  //
   225  // It indexes only charts that have been packaged (*.tgz).
   226  //
   227  // The index returned will be in an unsorted state
   228  func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
   229  	archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz"))
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	archives = append(archives, moreArchives...)
   238  
   239  	index := NewIndexFile()
   240  	for _, arch := range archives {
   241  		fname, err := filepath.Rel(dir, arch)
   242  		if err != nil {
   243  			return index, err
   244  		}
   245  
   246  		var parentDir string
   247  		parentDir, fname = filepath.Split(fname)
   248  		// filepath.Split appends an extra slash to the end of parentDir. We want to strip that out.
   249  		parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator))
   250  		parentURL, err := urlutil.URLJoin(baseURL, parentDir)
   251  		if err != nil {
   252  			parentURL = path.Join(baseURL, parentDir)
   253  		}
   254  
   255  		c, err := loader.Load(arch)
   256  		if err != nil {
   257  			// Assume this is not a chart.
   258  			continue
   259  		}
   260  		hash, err := provenance.DigestFile(arch)
   261  		if err != nil {
   262  			return index, err
   263  		}
   264  		index.Add(c.Metadata, fname, parentURL, hash)
   265  	}
   266  	return index, nil
   267  }
   268  
   269  // loadIndex loads an index file and does minimal validity checking.
   270  //
   271  // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
   272  func loadIndex(data []byte) (*IndexFile, error) {
   273  	i := &IndexFile{}
   274  	if err := yaml.Unmarshal(data, i); err != nil {
   275  		return i, err
   276  	}
   277  	i.SortEntries()
   278  	if i.APIVersion == "" {
   279  		return i, ErrNoAPIVersion
   280  	}
   281  	return i, nil
   282  }
   283  
   284  // unversionedEntry represents a deprecated pre-Alpha.5 format.
   285  //
   286  // This will be removed prior to v2.0.0
   287  type unversionedEntry struct {
   288  	Checksum  string          `json:"checksum"`
   289  	URL       string          `json:"url"`
   290  	Chartfile *chart.Metadata `json:"chartfile"`
   291  }
   292  
   293  // loadUnversionedIndex loads a pre-Alpha.5 index.yaml file.
   294  //
   295  // This format is deprecated. This function will be removed prior to v2.0.0.
   296  func loadUnversionedIndex(data []byte) (*IndexFile, error) {
   297  	fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'")
   298  	i := map[string]unversionedEntry{}
   299  
   300  	// This gets around an error in the YAML parser. Instead of parsing as YAML,
   301  	// we convert to JSON, and then decode again.
   302  	var err error
   303  	data, err = yaml.YAMLToJSON(data)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	if err := json.Unmarshal(data, &i); err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	if len(i) == 0 {
   312  		return nil, ErrNoAPIVersion
   313  	}
   314  	ni := NewIndexFile()
   315  	for n, item := range i {
   316  		if item.Chartfile == nil || item.Chartfile.Name == "" {
   317  			parts := strings.Split(n, "-")
   318  			ver := ""
   319  			if len(parts) > 1 {
   320  				ver = strings.TrimSuffix(parts[1], ".tgz")
   321  			}
   322  			item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver}
   323  		}
   324  		ni.Add(item.Chartfile, item.URL, "", item.Checksum)
   325  	}
   326  	return ni, nil
   327  }