Files
artifactapi/internal/virtual/helm_merger.go
T
unkin-agent 1ac8f60196
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Count only alias-reached nodes and expand chart aliases before index overrides
2026-10-17 12:52:35 +11:00

148 lines
3.5 KiB
Go

package virtual
import (
"fmt"
"log/slog"
"maps"
"slices"
"strings"
"gopkg.in/yaml.v3"
"git.unkin.net/unkin/artifactapi/internal/provider"
"git.unkin.net/unkin/artifactapi/pkg/models"
)
func init() {
RegisterMerger(models.PackageHelm, &HelmMerger{})
}
type HelmMerger struct{}
type helmIndex struct {
APIVersion string `yaml:"apiVersion"`
Entries map[string][]map[string]yaml.Node `yaml:"entries"`
Generated string `yaml:"generated,omitempty"`
}
// MergeIndexes keeps each chart version's fields as raw nodes so digest,
// apiVersion and the rest survive the merge untouched; only urls are rewritten.
func (m *HelmMerger) MergeIndexes(members []MemberIndex, proxyBaseURL string) ([]byte, error) {
merged := &helmIndex{
APIVersion: "v1",
Entries: make(map[string][]map[string]yaml.Node),
}
seen := map[string]map[string]bool{}
for _, member := range members {
var idx helmIndex
if err := yaml.Unmarshal(member.Body, &idx); err != nil {
continue
}
budget := provider.MaxAliasExpandedNodes
for _, chart := range slices.Sorted(maps.Keys(idx.Entries)) {
versions := idx.Entries[chart]
if seen[chart] == nil {
seen[chart] = map[string]bool{}
}
for _, raw := range versions {
ver, err := provider.ExpandYAMLAliases(raw, &budget)
if err != nil {
slog.Warn("helm merge: dropping chart entry", "member", member.RemoteName, "chart", chart, "error", err)
continue
}
version := ver["version"].Value
if seen[chart][version] {
continue
}
seen[chart][version] = true
if urlsNode, ok := ver["urls"]; ok && proxyBaseURL != "" {
var urls []string
if err := urlsNode.Decode(&urls); err != nil {
continue
}
rewriteHelmURLs(urls, member, proxyBaseURL)
var n yaml.Node
if err := n.Encode(urls); err != nil {
return nil, err
}
ver["urls"] = n
}
merged.Entries[chart] = append(merged.Entries[chart], ver)
}
}
}
return yaml.Marshal(merged)
}
func rewriteHelmURLs(urls []string, member MemberIndex, proxyBaseURL string) {
routePrefix := "remote"
if member.RepoType == "local" {
routePrefix = "local"
}
baseHost := extractHost(member.BaseURL)
for i, u := range urls {
if strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") {
if baseHost != "" && extractHost(u) != baseHost {
continue
}
relPath := extractPathRelativeToBase(u, member.BaseURL)
urls[i] = fmt.Sprintf("%s/api/v1/%s/%s/%s",
strings.TrimRight(proxyBaseURL, "/"),
routePrefix,
member.RemoteName,
relPath)
} else {
urls[i] = fmt.Sprintf("%s/api/v1/%s/%s/%s",
strings.TrimRight(proxyBaseURL, "/"),
routePrefix,
member.RemoteName,
u)
}
}
}
func extractHost(rawURL string) string {
idx := strings.Index(rawURL, "://")
if idx == -1 {
return ""
}
rest := rawURL[idx+3:]
slashIdx := strings.Index(rest, "/")
if slashIdx == -1 {
return rest
}
return rest[:slashIdx]
}
func extractPathRelativeToBase(rawURL, baseURL string) string {
fullPath := extractPath(rawURL)
basePath := extractPath(baseURL)
if basePath != "" {
basePath = strings.TrimRight(basePath, "/") + "/"
if strings.HasPrefix(fullPath, basePath) {
return fullPath[len(basePath):]
}
}
return fullPath
}
func extractPath(rawURL string) string {
idx := strings.Index(rawURL, "://")
if idx == -1 {
return rawURL
}
rest := rawURL[idx+3:]
slashIdx := strings.Index(rest, "/")
if slashIdx == -1 {
return ""
}
return rest[slashIdx+1:]
}