internal/helm/catalog.go
Rama main · réplica de lectura del repositorio original.
package helm
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"
"github.com/sophotechlabs/spinoza/internal/api"
"github.com/sophotechlabs/spinoza/internal/charts"
)
const (
searchLimit = 200
searchTimeout = 90 * time.Second
searchConcurrent = 8
valuesTimeout = 30 * time.Second
valuesTTL = 30 * time.Minute
valuesCapacity = 128
)
type repoHits struct {
entry RepoEntry
charts []charts.Chart
failure string
}
func (s *Service) SearchCharts(ctx context.Context, query string) (api.HelmChartSearch, error) {
out := api.HelmChartSearch{Query: query, Hits: []api.HelmChartHit{}}
if len(s.repos) == 0 {
out.Error = "no chart repositories are configured; add one with helm repo add"
return out, nil
}
if s.index == nil {
out.Error = "chart repositories are not wired up"
return out, nil
}
bounded, cancel := context.WithTimeout(ctx, searchTimeout)
defer cancel()
found := s.askEveryRepo(bounded, query)
failures := []string{}
for _, result := range found {
if result.failure != "" {
failures = append(failures, result.failure)
continue
}
for _, chart := range result.charts {
out.Hits = append(out.Hits, api.HelmChartHit{
Chart: chart.Name,
Version: chart.Version,
Description: chart.Description,
Repo: result.entry.Name,
URL: result.entry.Repo.URL,
})
}
}
slices.SortStableFunc(out.Hits, closerFirst(query))
if len(out.Hits) > searchLimit {
out.Hits = out.Hits[:searchLimit]
out.Truncated = true
}
out.Error = strings.Join(failures, "; ")
return out, nil
}
func (s *Service) askEveryRepo(ctx context.Context, query string) []repoHits {
found := make([]repoHits, len(s.repos))
tokens := make(chan struct{}, searchConcurrent)
var wg sync.WaitGroup
for i, entry := range s.repos {
wg.Go(func() {
tokens <- struct{}{}
defer func() { <-tokens }()
found[i] = s.askRepo(ctx, entry, query)
})
}
wg.Wait()
return found
}
func (s *Service) askRepo(ctx context.Context, entry RepoEntry, query string) repoHits {
if entry.Repo.OCI {
return s.probeRegistry(ctx, entry, query)
}
list, err := s.index.Search(ctx, entry.Repo, query, searchLimit)
if err != nil {
return repoHits{entry: entry, failure: fmt.Sprintf("%s: %v", repoLabel(entry), err)}
}
return repoHits{entry: entry, charts: list}
}
func (s *Service) probeRegistry(ctx context.Context, entry RepoEntry, query string) repoHits {
name := strings.TrimSpace(query)
if !nameFormat.MatchString(name) {
return repoHits{entry: entry}
}
tags, err := s.index.Versions(ctx, entry.Repo, name)
if err != nil {
return repoHits{entry: entry, failure: fmt.Sprintf("%s: %v", repoLabel(entry), err)}
}
if len(tags) == 0 {
return repoHits{entry: entry}
}
return repoHits{entry: entry, charts: []charts.Chart{{Name: name, Version: tags[0]}}}
}
func closerFirst(query string) func(left, right api.HelmChartHit) int {
needle := strings.ToLower(strings.TrimSpace(query))
return func(left, right api.HelmChartHit) int {
byRank := hitRank(left, needle) - hitRank(right, needle)
if byRank != 0 {
return byRank
}
if left.Chart != right.Chart {
return strings.Compare(left.Chart, right.Chart)
}
return strings.Compare(left.Repo, right.Repo)
}
}
func hitRank(hit api.HelmChartHit, needle string) int {
if needle == "" {
return 1
}
name := strings.ToLower(hit.Chart)
if name == needle {
return 0
}
if strings.HasPrefix(name, needle) {
return 1
}
if strings.Contains(name, needle) {
return 2
}
return 3
}
type ValuesRequest struct {
Chart string
Version string
RepoURL string
OCI bool
}
func (s *Service) ChartValues(ctx context.Context, req ValuesRequest) (api.HelmChartValues, error) {
if s.runner == nil {
return api.HelmChartValues{}, fmt.Errorf("%w: helm actions are not wired up", api.ErrInternal)
}
if !nameFormat.MatchString(req.Chart) {
return api.HelmChartValues{}, fmt.Errorf("%q is not a chart name", req.Chart)
}
if !charts.ValidVersion(req.Version) {
return api.HelmChartValues{}, fmt.Errorf("version %q is not a semantic version", req.Version)
}
repoErr := s.admitsRepository(req.RepoURL, req.OCI)
if repoErr != nil {
return api.HelmChartValues{}, repoErr
}
return s.cachedChartValues(ctx, req)
}
func (s *Service) cachedChartValues(ctx context.Context, req ValuesRequest) (api.HelmChartValues, error) {
now := s.valuesNow()
s.valuesMu.Lock()
s.pruneValues(now)
held, ok := s.values[req]
if ok {
s.valuesMu.Unlock()
return held.value, nil
}
flight, running := s.valuesInflight[req]
if running {
s.valuesMu.Unlock()
select {
case <-ctx.Done():
return api.HelmChartValues{}, ctx.Err()
case <-flight.done:
return flight.value, flight.err
}
}
flight = &valuesFlight{done: make(chan struct{})}
s.valuesInflight[req] = flight
s.valuesMu.Unlock()
value, err := s.fetchChartValues(ctx, req)
s.valuesMu.Lock()
delete(s.valuesInflight, req)
flight.value = value
flight.err = err
if err == nil {
s.values[req] = cachedValues{value: value, fetched: s.valuesNow()}
s.evictValues()
}
close(flight.done)
s.valuesMu.Unlock()
return value, err
}
func (s *Service) fetchChartValues(ctx context.Context, req ValuesRequest) (api.HelmChartValues, error) {
bounded, cancel := context.WithTimeout(ctx, valuesTimeout)
defer cancel()
args := []string{"show", "values", chartRef(req.Chart, req.RepoURL, req.OCI), "--version", req.Version}
if !req.OCI {
args = append(args, "--repo", req.RepoURL)
}
out, err := s.run(bounded, args, "")
if err != nil {
return api.HelmChartValues{}, err
}
return api.HelmChartValues{Chart: req.Chart, Version: req.Version, Values: out}, nil
}
func (s *Service) pruneValues(now time.Time) {
for req, held := range s.values {
if now.Sub(held.fetched) < valuesTTL {
continue
}
delete(s.values, req)
}
}
func (s *Service) evictValues() {
for len(s.values) > valuesCapacity {
var oldest ValuesRequest
var oldestAt time.Time
first := true
for req, held := range s.values {
if !first && !held.fetched.Before(oldestAt) {
continue
}
oldest = req
oldestAt = held.fetched
first = false
}
delete(s.values, oldest)
}
}