Ctrl K
ComunidadGuías…

internal/helm/catalog.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
	}
}