pull/32172/merge
EMMANUELA OPURUM 1 month ago committed by GitHub
commit 086daa1b4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,435 +1,495 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package repo package repo
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"slices" "slices"
"sort" "sort"
"strings" "strings"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v4/internal/fileutil" "helm.sh/helm/v4/internal/fileutil"
"helm.sh/helm/v4/internal/urlutil" "helm.sh/helm/v4/internal/urlutil"
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/chart/v2/loader"
"helm.sh/helm/v4/pkg/provenance" "helm.sh/helm/v4/pkg/provenance"
) )
// APIVersionV1 is the v1 API version for index and repository files. // APIVersionV1 is the v1 API version for index and repository files.
const APIVersionV1 = "v1" const APIVersionV1 = "v1"
var ( var (
// ErrNoAPIVersion indicates that an API version was not specified. // ErrNoAPIVersion indicates that an API version was not specified.
ErrNoAPIVersion = errors.New("no API version specified") ErrNoAPIVersion = errors.New("no API version specified")
// ErrNoChartVersion indicates that a chart with the given version is not found. // ErrNoChartVersion indicates that a chart with the given version is not found.
ErrNoChartVersion = errors.New("no chart version found") ErrNoChartVersion = errors.New("no chart version found")
// ErrNoChartName indicates that a chart with the given name is not found. // ErrNoChartName indicates that a chart with the given name is not found.
ErrNoChartName = errors.New("no chart name found") ErrNoChartName = errors.New("no chart name found")
// ErrEmptyIndexYaml indicates that the content of index.yaml is empty. // ErrEmptyIndexYaml indicates that the content of index.yaml is empty.
ErrEmptyIndexYaml = errors.New("empty index.yaml file") ErrEmptyIndexYaml = errors.New("empty index.yaml file")
) )
// ChartVersions is a list of versioned chart references. // ChartVersions is a list of versioned chart references.
// Implements a sorter on Version. // Implements a sorter on Version.
type ChartVersions []*ChartVersion type ChartVersions []*ChartVersion
// Len returns the length. // Len returns the length.
func (c ChartVersions) Len() int { return len(c) } func (c ChartVersions) Len() int { return len(c) }
// Swap swaps the position of two items in the versions slice. // Swap swaps the position of two items in the versions slice.
func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// Less returns true if the version of entry a is less than the version of entry b. // Less returns true if the version of entry a is less than the version of entry b.
func (c ChartVersions) Less(a, b int) bool { func (c ChartVersions) Less(a, b int) bool {
// Failed parse pushes to the back. // Failed parse pushes to the back.
i, err := semver.NewVersion(c[a].Version) i, err := semver.NewVersion(c[a].Version)
if err != nil { if err != nil {
return true return true
} }
j, err := semver.NewVersion(c[b].Version) j, err := semver.NewVersion(c[b].Version)
if err != nil { if err != nil {
return false return false
} }
return i.LessThan(j) return i.LessThan(j)
} }
// IndexFile represents the index file in a chart repository // IndexFile represents the index file in a chart repository
type IndexFile struct { type IndexFile struct {
// This is used ONLY for validation against chartmuseum's index files and is discarded after validation. // This is used ONLY for validation against chartmuseum's index files and is discarded after validation.
ServerInfo map[string]any `json:"serverInfo,omitempty"` ServerInfo map[string]any `json:"serverInfo,omitempty"`
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"` Generated time.Time `json:"generated"`
Entries map[string]ChartVersions `json:"entries"` Entries map[string]ChartVersions `json:"entries"`
PublicKeys []string `json:"publicKeys,omitempty"` PublicKeys []string `json:"publicKeys,omitempty"`
// Annotations are additional mappings uninterpreted by Helm. They are made available for // Annotations are additional mappings uninterpreted by Helm. They are made available for
// other applications to add information to the index file. // other applications to add information to the index file.
Annotations map[string]string `json:"annotations,omitempty"` Annotations map[string]string `json:"annotations,omitempty"`
} }
// NewIndexFile initializes an index. // NewIndexFile initializes an index.
func NewIndexFile() *IndexFile { func NewIndexFile() *IndexFile {
return &IndexFile{ return &IndexFile{
APIVersion: APIVersionV1, APIVersion: APIVersionV1,
Generated: time.Now(), Generated: time.Now(),
Entries: map[string]ChartVersions{}, Entries: map[string]ChartVersions{},
PublicKeys: []string{}, PublicKeys: []string{},
} }
} }
// LoadIndexFile takes a file at the given path and returns an IndexFile object // LoadIndexFile takes a file at the given path and returns an IndexFile object
func LoadIndexFile(path string) (*IndexFile, error) { func LoadIndexFile(path string) (*IndexFile, error) {
b, err := os.ReadFile(path) i, err := LoadIndexFileForEntries(path, nil)
if err != nil { if err != nil {
return nil, err var perr *os.PathError
} if errors.As(err, &perr) {
i, err := loadIndex(b, path) return nil, err
if err != nil { }
return nil, fmt.Errorf("error loading %s: %w", path, err) return nil, fmt.Errorf("error loading %s: %w", path, err)
} }
return i, nil return i, nil
} }
// MustAdd adds a file to the index // LoadIndexFileForEntries loads an index file but only retains entries
// This can leave the index in an unsorted state // matching the provided chart names. Unmatched entries are discarded
func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error { // immediately after unmarshalling and before validation, reducing heap
if i.Entries == nil { // usage when filtering to a small subset of charts in a large repository.
return errors.New("entries not initialized") // If names is nil or empty, all entries are loaded (equivalent to LoadIndexFile).
} func LoadIndexFileForEntries(path string, names []string) (*IndexFile, error) {
b, err := os.ReadFile(path)
if md.APIVersion == "" { if err != nil {
md.APIVersion = chart.APIVersionV1 return nil, err
} }
if err := md.Validate(); err != nil { if len(names) == 0 {
return fmt.Errorf("validate failed for %s: %w", filename, err) i, err := loadIndex(b, path)
} if err != nil {
return nil, err
u := filename }
if baseURL != "" { return i, nil
_, file := filepath.Split(filename) }
var err error i := &IndexFile{}
u, err = urlutil.URLJoin(baseURL, file) if len(b) == 0 {
if err != nil { return nil, ErrEmptyIndexYaml
u = path.Join(baseURL, file) }
} if err := jsonOrYamlUnmarshal(b, i); err != nil {
} return nil, err
cr := &ChartVersion{ }
URLs: []string{u}, // Build a new map containing only the requested entries to reduce heap usage.
Metadata: md, retained := make(map[string]ChartVersions, len(names))
Digest: digest, for _, n := range names {
Created: time.Now(), if cvs, ok := i.Entries[n]; ok {
} retained[n] = cvs
ee := i.Entries[md.Name] }
i.Entries[md.Name] = append(ee, cr) }
return nil i.Entries = retained
} // Validate and sort only the retained entries.
for name, cvs := range i.Entries {
// Add adds a file to the index and logs an error. for idx, v := range slices.Backward(cvs) {
// if v == nil {
// Deprecated: Use index.MustAdd instead. slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", path)
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { cvs = append(cvs[:idx], cvs[idx+1:]...)
if err := i.MustAdd(md, filename, baseURL, digest); err != nil { continue
slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err) }
} if v.Metadata == nil {
} v.Metadata = &chart.Metadata{}
}
// Has returns true if the index has an entry for a chart with the given name and exact version. if v.APIVersion == "" {
func (i IndexFile) Has(name, version string) bool { v.APIVersion = chart.APIVersionV1
_, err := i.Get(name, version) }
return err == nil if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil {
} slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", path, "error", err)
cvs = append(cvs[:idx], cvs[idx+1:]...)
// SortEntries sorts the entries by version in descending order. }
// }
// In canonical form, the individual version records should be sorted so that i.Entries[name] = cvs
// the most recent release for every version is in the 0th slot in the }
// Entries.ChartVersions array. That way, tooling can predict the newest i.SortEntries()
// version without needing to parse SemVers. if i.APIVersion == "" {
func (i IndexFile) SortEntries() { return i, ErrNoAPIVersion
for _, versions := range i.Entries { }
sort.Sort(sort.Reverse(versions)) return i, nil
} }
}
// MustAdd adds a file to the index
// isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10") // This can leave the index in an unsorted state
// rather than an exact version (e.g., "1.10.0"). func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
func isVersionRange(version string) bool { if i.Entries == nil {
if strings.ContainsAny(version, "^~<>=!*") || strings.Contains(version, "||") || strings.Contains(version, " - ") { return errors.New("entries not initialized")
return true }
}
core := version if md.APIVersion == "" {
if idx := strings.IndexAny(version, "-+"); idx != -1 { md.APIVersion = chart.APIVersionV1
core = version[:idx] }
} if err := md.Validate(); err != nil {
return strings.ContainsAny(core, "xX") return fmt.Errorf("validate failed for %s: %w", filename, err)
} }
// Get returns the ChartVersion for the given name. u := filename
// if baseURL != "" {
// If version is empty, this will return the chart with the latest stable version, _, file := filepath.Split(filename)
// prerelease versions will be skipped. var err error
func (i IndexFile) Get(name, version string) (*ChartVersion, error) { u, err = urlutil.URLJoin(baseURL, file)
vs, ok := i.Entries[name] if err != nil {
if !ok { u = path.Join(baseURL, file)
return nil, ErrNoChartName }
} }
if len(vs) == 0 { cr := &ChartVersion{
return nil, ErrNoChartVersion URLs: []string{u},
} Metadata: md,
Digest: digest,
var constraint *semver.Constraints Created: time.Now(),
if version == "" { }
constraint, _ = semver.NewConstraint("*") ee := i.Entries[md.Name]
} else { i.Entries[md.Name] = append(ee, cr)
var err error return nil
constraint, err = semver.NewConstraint(version) }
if err != nil {
return nil, err // Add adds a file to the index and logs an error.
} //
} // Deprecated: Use index.MustAdd instead.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
// when customer inputs specific version, check whether there's an exact match first if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
if version != "" { slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err)
for _, ver := range vs { }
if version == ver.Version { }
return ver, nil
} // Has returns true if the index has an entry for a chart with the given name and exact version.
} func (i IndexFile) Has(name, version string) bool {
} _, err := i.Get(name, version)
return err == nil
for _, ver := range vs { }
test, err := semver.NewVersion(ver.Version)
if err != nil { // SortEntries sorts the entries by version in descending order.
continue //
} // In canonical form, the individual version records should be sorted so that
// the most recent release for every version is in the 0th slot in the
if constraint.Check(test) { // Entries.ChartVersions array. That way, tooling can predict the newest
if version != "" && !isVersionRange(version) { // version without needing to parse SemVers.
slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) func (i IndexFile) SortEntries() {
} else if version != "" && isVersionRange(version) { for _, versions := range i.Entries {
slog.Debug("selected version matching constraint", "chart", name, "constraint", version, "selected", ver.Version) sort.Sort(sort.Reverse(versions))
} }
return ver, nil }
}
} // isVersionRange checks if the version string is a range constraint (e.g., "^1", "~1.10")
return nil, fmt.Errorf("no chart version found for %s-%s", name, version) // rather than an exact version (e.g., "1.10.0").
} func isVersionRange(version string) bool {
if strings.ContainsAny(version, "^~<>=!*") || strings.Contains(version, "||") || strings.Contains(version, " - ") {
// WriteFile writes an index file to the given destination path. return true
// }
// The mode on the file is set to 'mode'. core := version
func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { if idx := strings.IndexAny(version, "-+"); idx != -1 {
b, err := yaml.Marshal(i) core = version[:idx]
if err != nil { }
return err return strings.ContainsAny(core, "xX")
} }
return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
} // Get returns the ChartVersion for the given name.
//
// WriteJSONFile writes an index file in JSON format to the given destination // If version is empty, this will return the chart with the latest stable version,
// path. // prerelease versions will be skipped.
// func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
// The mode on the file is set to 'mode'. vs, ok := i.Entries[name]
func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error { if !ok {
b, err := json.MarshalIndent(i, "", " ") return nil, ErrNoChartName
if err != nil { }
return err if len(vs) == 0 {
} return nil, ErrNoChartVersion
return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode) }
}
var constraint *semver.Constraints
// Merge merges the given index file into this index. if version == "" {
// constraint, _ = semver.NewConstraint("*")
// This merges by name and version. } else {
// var err error
// If one of the entries in the given index does _not_ already exist, it is added. constraint, err = semver.NewConstraint(version)
// In all other cases, the existing record is preserved. if err != nil {
// return nil, err
// This can leave the index in an unsorted state }
func (i *IndexFile) Merge(f *IndexFile) { }
for _, cvs := range f.Entries {
for _, cv := range cvs { // when customer inputs specific version, check whether there's an exact match first
if !i.Has(cv.Name, cv.Version) { if version != "" {
e := i.Entries[cv.Name] for _, ver := range vs {
i.Entries[cv.Name] = append(e, cv) if version == ver.Version {
} return ver, nil
} }
} }
} }
// ChartVersion represents a chart entry in the IndexFile for _, ver := range vs {
type ChartVersion struct { test, err := semver.NewVersion(ver.Version)
*chart.Metadata if err != nil {
URLs []string `json:"urls"` continue
Created time.Time `json:"created"` }
Removed bool `json:"removed,omitempty"`
Digest string `json:"digest,omitempty"` if constraint.Check(test) {
if version != "" && !isVersionRange(version) {
// ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version)
// this with Digest. However, with a strict YAML parser enabled, a field must be } else if version != "" && isVersionRange(version) {
// present on the struct for backwards compatibility. slog.Debug("selected version matching constraint", "chart", name, "constraint", version, "selected", ver.Version)
ChecksumDeprecated string `json:"checksum,omitempty"` }
return ver, nil
// EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict }
// YAML parser enabled, this field must be present. }
EngineDeprecated string `json:"engine,omitempty"` return nil, fmt.Errorf("no chart version found for %s-%s", name, version)
}
// TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
// YAML parser enabled, this field must be present. // WriteFile writes an index file to the given destination path.
TillerVersionDeprecated string `json:"tillerVersion,omitempty"` //
// The mode on the file is set to 'mode'.
// URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However, func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
// with a strict YAML parser enabled, this must be present on the struct. b, err := yaml.Marshal(i)
URLDeprecated string `json:"url,omitempty"` if err != nil {
} return err
}
// IndexDirectory reads a (flat) directory and generates an index. return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
// }
// It indexes only charts that have been packaged (*.tgz).
// // WriteJSONFile writes an index file in JSON format to the given destination
// The index returned will be in an unsorted state // path.
func IndexDirectory(dir, baseURL string) (*IndexFile, error) { //
archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) // The mode on the file is set to 'mode'.
if err != nil { func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error {
return nil, err b, err := json.MarshalIndent(i, "", " ")
} if err != nil {
moreArchives, err := filepath.Glob(filepath.Join(dir, "**", "*.tgz")) return err
if err != nil { }
return nil, err return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
} }
archives = append(archives, moreArchives...)
// Merge merges the given index file into this index.
index := NewIndexFile() //
for _, arch := range archives { // This merges by name and version.
fname, err := filepath.Rel(dir, arch) //
if err != nil { // If one of the entries in the given index does _not_ already exist, it is added.
return index, err // In all other cases, the existing record is preserved.
} //
// This can leave the index in an unsorted state
var parentDir string func (i *IndexFile) Merge(f *IndexFile) {
parentDir, fname = filepath.Split(fname) for _, cvs := range f.Entries {
// filepath.Split appends an extra slash to the end of parentDir. We want to strip that out. for _, cv := range cvs {
parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator)) if !i.Has(cv.Name, cv.Version) {
parentURL, err := urlutil.URLJoin(baseURL, parentDir) e := i.Entries[cv.Name]
if err != nil { i.Entries[cv.Name] = append(e, cv)
parentURL = path.Join(baseURL, parentDir) }
} }
}
c, err := loader.Load(arch) }
if err != nil {
// Assume this is not a chart. // ChartVersion represents a chart entry in the IndexFile
continue type ChartVersion struct {
} *chart.Metadata
hash, err := provenance.DigestFile(arch) URLs []string `json:"urls"`
if err != nil { Created time.Time `json:"created"`
return index, err Removed bool `json:"removed,omitempty"`
} Digest string `json:"digest,omitempty"`
if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
return index, fmt.Errorf("failed adding to %s to index: %w", fname, err) // ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced
} // this with Digest. However, with a strict YAML parser enabled, a field must be
} // present on the struct for backwards compatibility.
return index, nil ChecksumDeprecated string `json:"checksum,omitempty"`
}
// EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
// loadIndex loads an index file and does minimal validity checking. // YAML parser enabled, this field must be present.
// EngineDeprecated string `json:"engine,omitempty"`
// The source parameter is only used for logging.
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. // TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
func loadIndex(data []byte, source string) (*IndexFile, error) { // YAML parser enabled, this field must be present.
i := &IndexFile{} TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
if len(data) == 0 { // URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However,
return i, ErrEmptyIndexYaml // with a strict YAML parser enabled, this must be present on the struct.
} URLDeprecated string `json:"url,omitempty"`
}
if err := jsonOrYamlUnmarshal(data, i); err != nil {
return i, err // IndexDirectory reads a (flat) directory and generates an index.
} //
// It indexes only charts that have been packaged (*.tgz).
for name, cvs := range i.Entries { //
for idx, v := range slices.Backward(cvs) { // The index returned will be in an unsorted state
if v == nil { func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source) archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
cvs = append(cvs[:idx], cvs[idx+1:]...) if err != nil {
continue return nil, err
} }
// When metadata section missing, initialize with no data moreArchives, err := filepath.Glob(filepath.Join(dir, "**", "*.tgz"))
if v.Metadata == nil { if err != nil {
v.Metadata = &chart.Metadata{} return nil, err
} }
if v.APIVersion == "" { archives = append(archives, moreArchives...)
v.APIVersion = chart.APIVersionV1
} index := NewIndexFile()
if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil { for _, arch := range archives {
slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err) fname, err := filepath.Rel(dir, arch)
cvs = append(cvs[:idx], cvs[idx+1:]...) if err != nil {
} return index, err
} }
// adjust slice to only contain a set of valid versions
i.Entries[name] = cvs var parentDir string
} parentDir, fname = filepath.Split(fname)
i.SortEntries() // filepath.Split appends an extra slash to the end of parentDir. We want to strip that out.
if i.APIVersion == "" { parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator))
return i, ErrNoAPIVersion parentURL, err := urlutil.URLJoin(baseURL, parentDir)
} if err != nil {
return i, nil parentURL = path.Join(baseURL, parentDir)
} }
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML c, err := loader.Load(arch)
// into the provided interface. if err != nil {
// // Assume this is not a chart.
// It automatically detects whether the data is in JSON or YAML format by continue
// checking its validity as JSON. If the data is valid JSON, it will use the }
// `encoding/json` package to unmarshal it. Otherwise, it will use the hash, err := provenance.DigestFile(arch)
// `sigs.k8s.io/yaml` package to unmarshal the YAML data. if err != nil {
func jsonOrYamlUnmarshal(b []byte, i any) error { return index, err
if json.Valid(b) { }
return json.Unmarshal(b, i) if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
} return index, fmt.Errorf("failed adding to %s to index: %w", fname, err)
return yaml.UnmarshalStrict(b, i) }
} }
return index, nil
// ignoreSkippableChartValidationError inspect the given error and returns nil if }
// the error isn't important for index loading
// // loadIndex loads an index file and does minimal validity checking.
// In particular, charts may introduce validations that don't impact repository indexes //
// And repository indexes may be generated by older/non-compliant software, which doesn't // The source parameter is only used for logging.
// conform to all validations. // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func ignoreSkippableChartValidationError(err error) error { func loadIndex(data []byte, source string) (*IndexFile, error) {
var verr chart.ValidationError i := &IndexFile{}
if !errors.As(err, &verr) {
return err if len(data) == 0 {
} return i, ErrEmptyIndexYaml
}
// https://github.com/helm/helm/issues/12748 (JFrog repository strips alias field)
if strings.HasPrefix(verr.Error(), "validation: more than one dependency with name or alias") { if err := jsonOrYamlUnmarshal(data, i); err != nil {
return nil return i, err
} }
return err for name, cvs := range i.Entries {
} for idx, v := range slices.Backward(cvs) {
if v == nil {
slog.Warn("skipping loading invalid entry for chart: empty entry", "name", name, "source", source)
cvs = append(cvs[:idx], cvs[idx+1:]...)
continue
}
// When metadata section missing, initialize with no data
if v.Metadata == nil {
v.Metadata = &chart.Metadata{}
}
if v.APIVersion == "" {
v.APIVersion = chart.APIVersionV1
}
if err := v.Validate(); ignoreSkippableChartValidationError(err) != nil {
slog.Warn("skipping loading invalid entry for chart", "name", name, "version", v.Version, "source", source, "error", err)
cvs = append(cvs[:idx], cvs[idx+1:]...)
}
}
// adjust slice to only contain a set of valid versions
i.Entries[name] = cvs
}
i.SortEntries()
if i.APIVersion == "" {
return i, ErrNoAPIVersion
}
return i, nil
}
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
// into the provided interface.
//
// It automatically detects whether the data is in JSON or YAML format by
// checking its validity as JSON. If the data is valid JSON, it will use the
// `encoding/json` package to unmarshal it. Otherwise, it will use the
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
func jsonOrYamlUnmarshal(b []byte, i any) error {
if json.Valid(b) {
return json.Unmarshal(b, i)
}
return yaml.UnmarshalStrict(b, i)
}
// ignoreSkippableChartValidationError inspect the given error and returns nil if
// the error isn't important for index loading
//
// In particular, charts may introduce validations that don't impact repository indexes
// And repository indexes may be generated by older/non-compliant software, which doesn't
// conform to all validations.
func ignoreSkippableChartValidationError(err error) error {
var verr chart.ValidationError
if !errors.As(err, &verr) {
return err
}
// https://github.com/helm/helm/issues/12748 (JFrog repository strips alias field)
if strings.HasPrefix(verr.Error(), "validation: more than one dependency with name or alias") {
return nil
}
return err
}

@ -622,3 +622,62 @@ func TestIsVersionRange(t *testing.T) {
}) })
} }
} }
func TestLoadIndexFileForEntries(t *testing.T) {
// nil names loads all entries
t.Run("nil names loads all", func(t *testing.T) {
i, err := LoadIndexFileForEntries("testdata/local-index.yaml", nil)
if err != nil {
t.Fatal(err)
}
if len(i.Entries) == 0 {
t.Fatal("expected entries, got none")
}
})
t.Run("empty names loads all", func(t *testing.T) {
i, err := LoadIndexFileForEntries("testdata/local-index.yaml", []string{})
if err != nil {
t.Fatal(err)
}
if len(i.Entries) == 0 {
t.Fatal("expected entries, got none")
}
})
// filter to a specific chart
t.Run("filter to one entry", func(t *testing.T) {
i, err := LoadIndexFileForEntries("testdata/local-index.yaml", []string{"alpine"})
if err != nil {
t.Fatal(err)
}
if len(i.Entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(i.Entries))
}
if _, ok := i.Entries["alpine"]; !ok {
t.Fatal("expected 'alpine' entry")
}
})
// no matches returns empty entries
t.Run("no matches returns empty", func(t *testing.T) {
i, err := LoadIndexFileForEntries("testdata/local-index.yaml", []string{"nonexistent-chart"})
if err != nil {
t.Fatal(err)
}
if len(i.Entries) != 0 {
t.Fatalf("expected 0 entries, got %d", len(i.Entries))
}
})
// partial match retains only matched entries
t.Run("partial match", func(t *testing.T) {
i, err := LoadIndexFileForEntries("testdata/local-index.yaml", []string{"nginx", "nonexistent-chart"})
if err != nil {
t.Fatal(err)
}
if len(i.Entries) != 1 {
t.Fatalf("expected 1 entry, got %d", len(i.Entries))
}
})
}

Loading…
Cancel
Save