source-controller/internal/helm/repository.go

219 lines
6.1 KiB
Go

/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package helm
import (
"bytes"
"fmt"
"io"
"net/url"
"path"
"sort"
"strings"
"github.com/Masterminds/semver/v3"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/version"
)
// ChartRepository represents a Helm chart repository, and the configuration
// required to download the chart index, and charts from the repository.
type ChartRepository struct {
URL string
Index *repo.IndexFile
Client getter.Getter
Options []getter.Option
}
// NewChartRepository constructs and returns a new ChartRepository with
// the ChartRepository.Client configured to the getter.Getter for the
// repository URL scheme. It returns an error on URL parsing failures,
// or if there is no getter available for the scheme.
func NewChartRepository(repositoryURL string, providers getter.Providers, opts []getter.Option) (*ChartRepository, error) {
u, err := url.Parse(repositoryURL)
if err != nil {
return nil, err
}
c, err := providers.ByScheme(u.Scheme)
if err != nil {
return nil, err
}
return &ChartRepository{
URL: repositoryURL,
Client: c,
Options: opts,
}, nil
}
// Get returns the repo.ChartVersion for the given name, the version is expected
// to be a semver.Constraints compatible string. If version is empty, the latest
// stable version will be returned and prerelease versions will be ignored.
func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) {
cvs, ok := r.Index.Entries[name]
if !ok {
return nil, repo.ErrNoChartName
}
if len(cvs) == 0 {
return nil, repo.ErrNoChartVersion
}
// Check for exact matches first
if len(ver) != 0 {
for _, cv := range cvs {
if ver == cv.Version {
return cv, nil
}
}
}
// Continue to look for a (semantic) version match
verConstraint, err := semver.NewConstraint("*")
if err != nil {
return nil, err
}
latestStable := len(ver) == 0 || ver == "*"
if !latestStable {
verConstraint, err = semver.NewConstraint(ver)
if err != nil {
return nil, err
}
}
// Filter out chart versions that doesn't satisfy constraints if any,
// parse semver and build a lookup table
var matchedVersions semver.Collection
lookup := make(map[*semver.Version]*repo.ChartVersion)
for _, cv := range cvs {
v, err := version.ParseVersion(cv.Version)
if err != nil {
continue
}
if !verConstraint.Check(v) {
continue
}
matchedVersions = append(matchedVersions, v)
lookup[v] = cv
}
if len(matchedVersions) == 0 {
return nil, fmt.Errorf("no chart version found for %s-%s", name, ver)
}
// Sort versions
sort.SliceStable(matchedVersions, func(i, j int) bool {
// Reverse
return !(func() bool {
left := matchedVersions[i]
right := matchedVersions[j]
if !left.Equal(right) {
return left.LessThan(right)
}
// Having chart creation timestamp at our disposal, we put package with the
// same version into a chronological order. This is especially important for
// versions that differ only by build metadata, because it is not considered
// a part of the comparable version in Semver
return lookup[left].Created.Before(lookup[right].Created)
})()
})
latest := matchedVersions[0]
return lookup[latest], nil
}
// DownloadChart confirms the given repo.ChartVersion has a downloadable URL,
// and then attempts to download the chart using the Client and Options of the
// ChartRepository. It returns a bytes.Buffer containing the chart data.
func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error) {
if len(chart.URLs) == 0 {
return nil, fmt.Errorf("chart %q has no downloadable URLs", chart.Name)
}
// TODO(hidde): according to the Helm source the first item is not
// always the correct one to pick, check for updates once in awhile.
// Ref: https://github.com/helm/helm/blob/v3.3.0/pkg/downloader/chart_downloader.go#L241
ref := chart.URLs[0]
u, err := url.Parse(ref)
if err != nil {
err = fmt.Errorf("invalid chart URL format '%s': %w", ref, err)
return nil, err
}
// Prepend the chart repository base URL if the URL is relative
if !u.IsAbs() {
repoURL, err := url.Parse(r.URL)
if err != nil {
err = fmt.Errorf("invalid chart repository URL format '%s': %w", r.URL, err)
return nil, err
}
q := repoURL.Query()
// Trailing slash is required for ResolveReference to work
repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/"
u = repoURL.ResolveReference(u)
u.RawQuery = q.Encode()
}
return r.Client.Get(u.String(), r.Options...)
}
// LoadIndex loads the given bytes into the Index while performing
// minimal validity checks. It fails if the API version is not set
// (repo.ErrNoAPIVersion), or if the unmarshal fails.
//
// The logic is derived from and on par with:
// https://github.com/helm/helm/blob/v3.3.4/pkg/repo/index.go#L301
func (r *ChartRepository) LoadIndex(b []byte) error {
i := &repo.IndexFile{}
if err := yaml.UnmarshalStrict(b, i); err != nil {
return err
}
if i.APIVersion == "" {
return repo.ErrNoAPIVersion
}
i.SortEntries()
r.Index = i
return nil
}
// DownloadIndex attempts to download the chart repository index using
// the Client and set Options, and loads the index file into the Index.
// It returns an error on URL parsing and Client failures.
func (r *ChartRepository) DownloadIndex() error {
u, err := url.Parse(r.URL)
if err != nil {
return err
}
u.RawPath = path.Join(u.RawPath, "index.yaml")
u.Path = path.Join(u.Path, "index.yaml")
res, err := r.Client.Get(u.String(), r.Options...)
if err != nil {
return err
}
b, err := io.ReadAll(res)
if err != nil {
return err
}
return r.LoadIndex(b)
}