Files

149 lines
4.2 KiB
Go

package holdclient
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"atcr.io/pkg/atproto"
)
// HelmChartMeta is the parsed Chart.yaml-equivalent metadata extracted from a
// helm chart's OCI config blob (media type
// application/vnd.cncf.helm.config.v1+json). The helm config blob is Chart.yaml
// rendered as JSON, so the field names mirror Chart.yaml.
type HelmChartMeta struct {
Name string
Version string
AppVersion string
Type string // "application" | "library" (empty in older charts; treat as "application")
Description string
KubeVersion string
Home string
Icon string
Sources []string
Keywords []string
Maintainers []HelmMaintainer
Dependencies []HelmDependency
Annotations map[string]string
Deprecated bool
}
type HelmMaintainer struct {
Name string
Email string
URL string
}
type HelmDependency struct {
Name string
Version string
Repository string
Alias string
Condition string
}
// helmConfigJSON matches the on-the-wire shape of a helm OCI config blob.
// Field names track Chart.yaml's JSON form (lowerCamelCase for some fields,
// kebab-case for none — helm's CLI marshals Chart.yaml struct directly).
type helmConfigJSON struct {
Name string `json:"name"`
Version string `json:"version"`
AppVersion string `json:"appVersion"`
Type string `json:"type"`
Description string `json:"description"`
KubeVersion string `json:"kubeVersion"`
Home string `json:"home"`
Icon string `json:"icon"`
Sources []string `json:"sources"`
Keywords []string `json:"keywords"`
Maintainers []helmMaintainerJSON `json:"maintainers"`
Dependencies []helmDependencyJSON `json:"dependencies"`
Annotations map[string]string `json:"annotations"`
Deprecated bool `json:"deprecated"`
}
type helmMaintainerJSON struct {
Name string `json:"name"`
Email string `json:"email"`
URL string `json:"url"`
}
type helmDependencyJSON struct {
Name string `json:"name"`
Version string `json:"version"`
Repository string `json:"repository"`
Alias string `json:"alias"`
Condition string `json:"condition"`
}
// FetchHelmChartMeta fetches a helm chart's config blob from the hold and
// parses it as Chart.yaml metadata. Uses the same getImageConfig XRPC as
// FetchImageConfig but applies a helm-specific schema to the JSON.
func FetchHelmChartMeta(ctx context.Context, holdURL, manifestDigest string) (*HelmChartMeta, error) {
reqURL := fmt.Sprintf("%s%s?digest=%s",
strings.TrimSuffix(holdURL, "/"),
atproto.HoldGetImageConfig,
url.QueryEscape(manifestDigest),
)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch helm chart config: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("hold returned status %d for %s", resp.StatusCode, reqURL)
}
var record struct {
ConfigJSON string `json:"configJson"`
}
if err := json.NewDecoder(resp.Body).Decode(&record); err != nil {
return nil, fmt.Errorf("parse image config response: %w", err)
}
var raw helmConfigJSON
if err := json.Unmarshal([]byte(record.ConfigJSON), &raw); err != nil {
return nil, fmt.Errorf("parse helm chart config JSON: %w", err)
}
meta := &HelmChartMeta{
Name: raw.Name,
Version: raw.Version,
AppVersion: raw.AppVersion,
Type: raw.Type,
Description: raw.Description,
KubeVersion: raw.KubeVersion,
Home: raw.Home,
Icon: raw.Icon,
Sources: raw.Sources,
Keywords: raw.Keywords,
Annotations: raw.Annotations,
Deprecated: raw.Deprecated,
}
if meta.Type == "" {
meta.Type = "application"
}
for _, m := range raw.Maintainers {
meta.Maintainers = append(meta.Maintainers, HelmMaintainer(m))
}
for _, d := range raw.Dependencies {
meta.Dependencies = append(meta.Dependencies, HelmDependency(d))
}
return meta, nil
}