github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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  	"bytes"
    21  	"io/ioutil"
    22  	"log"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/Masterminds/semver/v3"
    31  	"github.com/pkg/errors"
    32  	"sigs.k8s.io/yaml"
    33  
    34  	"github.com/stefanmcshane/helm/internal/fileutil"
    35  	"github.com/stefanmcshane/helm/internal/urlutil"
    36  	"github.com/stefanmcshane/helm/pkg/chart"
    37  	"github.com/stefanmcshane/helm/pkg/chart/loader"
    38  	"github.com/stefanmcshane/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  	// ErrEmptyIndexYaml indicates that the content of index.yaml is empty.
    54  	ErrEmptyIndexYaml = errors.New("empty index.yaml file")
    55  )
    56  
    57  // ChartVersions is a list of versioned chart references.
    58  // Implements a sorter on Version.
    59  type ChartVersions []*ChartVersion
    60  
    61  // Len returns the length.
    62  func (c ChartVersions) Len() int { return len(c) }
    63  
    64  // Swap swaps the position of two items in the versions slice.
    65  func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
    66  
    67  // Less returns true if the version of entry a is less than the version of entry b.
    68  func (c ChartVersions) Less(a, b int) bool {
    69  	// Failed parse pushes to the back.
    70  	i, err := semver.NewVersion(c[a].Version)
    71  	if err != nil {
    72  		return true
    73  	}
    74  	j, err := semver.NewVersion(c[b].Version)
    75  	if err != nil {
    76  		return false
    77  	}
    78  	return i.LessThan(j)
    79  }
    80  
    81  // IndexFile represents the index file in a chart repository
    82  type IndexFile struct {
    83  	// This is used ONLY for validation against chartmuseum's index files and is discarded after validation.
    84  	ServerInfo map[string]interface{}   `json:"serverInfo,omitempty"`
    85  	APIVersion string                   `json:"apiVersion"`
    86  	Generated  time.Time                `json:"generated"`
    87  	Entries    map[string]ChartVersions `json:"entries"`
    88  	PublicKeys []string                 `json:"publicKeys,omitempty"`
    89  
    90  	// Annotations are additional mappings uninterpreted by Helm. They are made available for
    91  	// other applications to add information to the index file.
    92  	Annotations map[string]string `json:"annotations,omitempty"`
    93  }
    94  
    95  // NewIndexFile initializes an index.
    96  func NewIndexFile() *IndexFile {
    97  	return &IndexFile{
    98  		APIVersion: APIVersionV1,
    99  		Generated:  time.Now(),
   100  		Entries:    map[string]ChartVersions{},
   101  		PublicKeys: []string{},
   102  	}
   103  }
   104  
   105  // LoadIndexFile takes a file at the given path and returns an IndexFile object
   106  func LoadIndexFile(path string) (*IndexFile, error) {
   107  	b, err := ioutil.ReadFile(path)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	i, err := loadIndex(b, path)
   112  	if err != nil {
   113  		return nil, errors.Wrapf(err, "error loading %s", path)
   114  	}
   115  	return i, nil
   116  }
   117  
   118  // MustAdd adds a file to the index
   119  // This can leave the index in an unsorted state
   120  func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
   121  	if md.APIVersion == "" {
   122  		md.APIVersion = chart.APIVersionV1
   123  	}
   124  	if err := md.Validate(); err != nil {
   125  		return errors.Wrapf(err, "validate failed for %s", filename)
   126  	}
   127  
   128  	u := filename
   129  	if baseURL != "" {
   130  		_, file := filepath.Split(filename)
   131  		var err error
   132  		u, err = urlutil.URLJoin(baseURL, file)
   133  		if err != nil {
   134  			u = path.Join(baseURL, file)
   135  		}
   136  	}
   137  	cr := &ChartVersion{
   138  		URLs:     []string{u},
   139  		Metadata: md,
   140  		Digest:   digest,
   141  		Created:  time.Now(),
   142  	}
   143  	ee := i.Entries[md.Name]
   144  	i.Entries[md.Name] = append(ee, cr)
   145  	return nil
   146  }
   147  
   148  // Add adds a file to the index and logs an error.
   149  //
   150  // Deprecated: Use index.MustAdd instead.
   151  func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
   152  	if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
   153  		log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err)
   154  	}
   155  }
   156  
   157  // Has returns true if the index has an entry for a chart with the given name and exact version.
   158  func (i IndexFile) Has(name, version string) bool {
   159  	_, err := i.Get(name, version)
   160  	return err == nil
   161  }
   162  
   163  // SortEntries sorts the entries by version in descending order.
   164  //
   165  // In canonical form, the individual version records should be sorted so that
   166  // the most recent release for every version is in the 0th slot in the
   167  // Entries.ChartVersions array. That way, tooling can predict the newest
   168  // version without needing to parse SemVers.
   169  func (i IndexFile) SortEntries() {
   170  	for _, versions := range i.Entries {
   171  		sort.Sort(sort.Reverse(versions))
   172  	}
   173  }
   174  
   175  // Get returns the ChartVersion for the given name.
   176  //
   177  // If version is empty, this will return the chart with the latest stable version,
   178  // prerelease versions will be skipped.
   179  func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
   180  	vs, ok := i.Entries[name]
   181  	if !ok {
   182  		return nil, ErrNoChartName
   183  	}
   184  	if len(vs) == 0 {
   185  		return nil, ErrNoChartVersion
   186  	}
   187  
   188  	var constraint *semver.Constraints
   189  	if version == "" {
   190  		constraint, _ = semver.NewConstraint("*")
   191  	} else {
   192  		var err error
   193  		constraint, err = semver.NewConstraint(version)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  	}
   198  
   199  	// when customer input exact version, check whether have exact match one first
   200  	if len(version) != 0 {
   201  		for _, ver := range vs {
   202  			if version == ver.Version {
   203  				return ver, nil
   204  			}
   205  		}
   206  	}
   207  
   208  	for _, ver := range vs {
   209  		test, err := semver.NewVersion(ver.Version)
   210  		if err != nil {
   211  			continue
   212  		}
   213  
   214  		if constraint.Check(test) {
   215  			return ver, nil
   216  		}
   217  	}
   218  	return nil, errors.Errorf("no chart version found for %s-%s", name, version)
   219  }
   220  
   221  // WriteFile writes an index file to the given destination path.
   222  //
   223  // The mode on the file is set to 'mode'.
   224  func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
   225  	b, err := yaml.Marshal(i)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
   230  }
   231  
   232  // Merge merges the given index file into this index.
   233  //
   234  // This merges by name and version.
   235  //
   236  // If one of the entries in the given index does _not_ already exist, it is added.
   237  // In all other cases, the existing record is preserved.
   238  //
   239  // This can leave the index in an unsorted state
   240  func (i *IndexFile) Merge(f *IndexFile) {
   241  	for _, cvs := range f.Entries {
   242  		for _, cv := range cvs {
   243  			if !i.Has(cv.Name, cv.Version) {
   244  				e := i.Entries[cv.Name]
   245  				i.Entries[cv.Name] = append(e, cv)
   246  			}
   247  		}
   248  	}
   249  }
   250  
   251  // ChartVersion represents a chart entry in the IndexFile
   252  type ChartVersion struct {
   253  	*chart.Metadata
   254  	URLs    []string  `json:"urls"`
   255  	Created time.Time `json:"created,omitempty"`
   256  	Removed bool      `json:"removed,omitempty"`
   257  	Digest  string    `json:"digest,omitempty"`
   258  
   259  	// ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced
   260  	// this with Digest. However, with a strict YAML parser enabled, a field must be
   261  	// present on the struct for backwards compatibility.
   262  	ChecksumDeprecated string `json:"checksum,omitempty"`
   263  
   264  	// EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
   265  	// YAML parser enabled, this field must be present.
   266  	EngineDeprecated string `json:"engine,omitempty"`
   267  
   268  	// TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
   269  	// YAML parser enabled, this field must be present.
   270  	TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
   271  
   272  	// URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However,
   273  	// with a strict YAML parser enabled, this must be present on the struct.
   274  	URLDeprecated string `json:"url,omitempty"`
   275  }
   276  
   277  // IndexDirectory reads a (flat) directory and generates an index.
   278  //
   279  // It indexes only charts that have been packaged (*.tgz).
   280  //
   281  // The index returned will be in an unsorted state
   282  func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
   283  	archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  	moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz"))
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	archives = append(archives, moreArchives...)
   292  
   293  	index := NewIndexFile()
   294  	for _, arch := range archives {
   295  		fname, err := filepath.Rel(dir, arch)
   296  		if err != nil {
   297  			return index, err
   298  		}
   299  
   300  		var parentDir string
   301  		parentDir, fname = filepath.Split(fname)
   302  		// filepath.Split appends an extra slash to the end of parentDir. We want to strip that out.
   303  		parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator))
   304  		parentURL, err := urlutil.URLJoin(baseURL, parentDir)
   305  		if err != nil {
   306  			parentURL = path.Join(baseURL, parentDir)
   307  		}
   308  
   309  		c, err := loader.Load(arch)
   310  		if err != nil {
   311  			// Assume this is not a chart.
   312  			continue
   313  		}
   314  		hash, err := provenance.DigestFile(arch)
   315  		if err != nil {
   316  			return index, err
   317  		}
   318  		if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
   319  			return index, errors.Wrapf(err, "failed adding to %s to index", fname)
   320  		}
   321  	}
   322  	return index, nil
   323  }
   324  
   325  // loadIndex loads an index file and does minimal validity checking.
   326  //
   327  // The source parameter is only used for logging.
   328  // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
   329  func loadIndex(data []byte, source string) (*IndexFile, error) {
   330  	i := &IndexFile{}
   331  
   332  	if len(data) == 0 {
   333  		return i, ErrEmptyIndexYaml
   334  	}
   335  
   336  	if err := yaml.UnmarshalStrict(data, i); err != nil {
   337  		return i, err
   338  	}
   339  
   340  	for name, cvs := range i.Entries {
   341  		for idx := len(cvs) - 1; idx >= 0; idx-- {
   342  			if cvs[idx].APIVersion == "" {
   343  				cvs[idx].APIVersion = chart.APIVersionV1
   344  			}
   345  			if err := cvs[idx].Validate(); err != nil {
   346  				log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)
   347  				cvs = append(cvs[:idx], cvs[idx+1:]...)
   348  			}
   349  		}
   350  	}
   351  	i.SortEntries()
   352  	if i.APIVersion == "" {
   353  		return i, ErrNoAPIVersion
   354  	}
   355  	return i, nil
   356  }