mirror of
https://github.com/Luzifer/go-latestver.git
synced 2024-12-20 18:41:17 +00:00
163 lines
4.9 KiB
Go
163 lines
4.9 KiB
Go
package fetcher
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Luzifer/go-latestver/internal/database"
|
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
|
"github.com/pkg/errors"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"helm.sh/helm/v3/pkg/repo"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
/*
|
|
* @module helm
|
|
* @module_desc Fetches the index file of a Helm Repo and yields the latest Helm-Chart version
|
|
*/
|
|
|
|
type (
|
|
// HELMFetcher implements the fetcher interface to retrieve a version from a Helm Repo
|
|
HELMFetcher struct{}
|
|
)
|
|
|
|
func init() { registerFetcher("helm", func() Fetcher { return &HELMFetcher{} }) }
|
|
|
|
// FetchVersion retrieves the latest version for the catalog entry
|
|
func (h HELMFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
|
vers, err := h.getChartVersionsFromRepo(ctx, attrs.MustString("repo", nil), attrs.MustString("chart", nil))
|
|
if err != nil {
|
|
return "", time.Time{}, fmt.Errorf("fetching chart versions: %w", err)
|
|
}
|
|
|
|
if vers == nil {
|
|
return "", time.Time{}, fmt.Errorf("chart not found in repo")
|
|
}
|
|
|
|
return vers[0].Version, vers[0].Created, nil
|
|
}
|
|
|
|
// Links retrieves a collection of links for the fetcher
|
|
func (HELMFetcher) Links(_ *fieldcollection.FieldCollection) []database.CatalogLink { return nil }
|
|
|
|
// Validate validates the configuration given to the fetcher
|
|
func (HELMFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
|
// @attr repo required string "" URL of the repo (i.e. "https://grafana.github.io/helm-charts")
|
|
if v, err := attrs.String("repo"); err != nil || v == "" {
|
|
return errors.New("repo is expected to be non-empty string")
|
|
}
|
|
|
|
// @attr chart required string "" Chart to fetch the version of (i.e. "grafana")
|
|
if v, err := attrs.String("chart"); err != nil || v == "" {
|
|
return errors.New("chart is expected to be non-empty string")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h HELMFetcher) getChartVersionsFromRepo(ctx context.Context, repoURL, chartName string) (repo.ChartVersions, error) {
|
|
if !strings.HasSuffix(repoURL, "/index.yaml") {
|
|
repoURL = strings.Join([]string{
|
|
strings.TrimRight(repoURL, "/"),
|
|
"index.yaml",
|
|
}, "/")
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, repoURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("executing request: %w", err)
|
|
}
|
|
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected HTTP status %d", resp.StatusCode)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
if _, err = io.Copy(buf, resp.Body); err != nil {
|
|
return nil, fmt.Errorf("reading index content: %w", err)
|
|
}
|
|
|
|
index, err := h.loadIndex(buf.Bytes(), repoURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing index file: %w", err)
|
|
}
|
|
|
|
return index.Entries[chartName], nil
|
|
}
|
|
|
|
/*
|
|
* Load functions taken from Helm v3 library as they are only defined
|
|
* internally and not exposed
|
|
*
|
|
* https://github.com/helm/helm/blob/v3.14.2/pkg/repo/index.go#L341-L394
|
|
*/
|
|
|
|
// loadIndex loads an index file and does minimal validity checking.
|
|
//
|
|
// The source parameter is only used for logging.
|
|
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
|
|
func (h HELMFetcher) loadIndex(data []byte, source string) (*repo.IndexFile, error) {
|
|
i := &repo.IndexFile{}
|
|
|
|
if len(data) == 0 {
|
|
return i, repo.ErrEmptyIndexYaml
|
|
}
|
|
|
|
if err := h.jsonOrYamlUnmarshal(data, i); err != nil {
|
|
return i, err
|
|
}
|
|
|
|
for name, cvs := range i.Entries {
|
|
for idx := len(cvs) - 1; idx >= 0; idx-- {
|
|
if cvs[idx] == nil {
|
|
log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)
|
|
continue
|
|
}
|
|
// When metadata section missing, initialize with no data
|
|
if cvs[idx].Metadata == nil {
|
|
cvs[idx].Metadata = &chart.Metadata{}
|
|
}
|
|
if cvs[idx].APIVersion == "" {
|
|
cvs[idx].APIVersion = chart.APIVersionV1
|
|
}
|
|
if err := cvs[idx].Validate(); err != nil {
|
|
log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)
|
|
cvs = append(cvs[:idx], cvs[idx+1:]...)
|
|
}
|
|
}
|
|
}
|
|
i.SortEntries()
|
|
if i.APIVersion == "" {
|
|
return i, repo.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 (HELMFetcher) jsonOrYamlUnmarshal(b []byte, i interface{}) error {
|
|
if json.Valid(b) {
|
|
return json.Unmarshal(b, i) //nolint:wrapcheck // Fine at this point
|
|
}
|
|
return yaml.UnmarshalStrict(b, i) //nolint:wrapcheck // Fine at this point
|
|
}
|