mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
Mirroring pkg:helm PURLs failed with an unsupported ecosystem error. The
namespace now names the upstream.helm repository, as the Helm handler
caches charts per repository: pkg:helm/{repository}/{chart}@{version}.
The chart is found in the repository's index through the handler's own
index lookup, fetched from the first URL the release lists, as helm does,
checked against the listed digest and cached under the repository,
digest and filename the handler serves it from. The index goes through
the metadata cache, so helm finds the chart there while the repository is
down. Unversioned PURLs list every version in the index.
Charts without a repository, from an unconfigured one, or listed in an
OCI registry are refused; the last are pulled through /v2, and the error
names the pkg:oci PURL that mirrors them.
147 lines
5 KiB
Go
147 lines
5 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
|
)
|
|
|
|
// helmUpstream serves an HTTP Helm repository listing demo 1.0.0 at a
|
|
// relative URL and demo 2.0.0 in an OCI registry, and the 1.0.0 chart.
|
|
type helmUpstream struct {
|
|
*httptest.Server
|
|
chart string
|
|
digest string
|
|
chartHits atomic.Int32
|
|
}
|
|
|
|
func newHelmUpstream(t *testing.T) *helmUpstream {
|
|
t.Helper()
|
|
u := &helmUpstream{chart: "demo chart archive"}
|
|
sum := sha256.Sum256([]byte(u.chart))
|
|
u.digest = hex.EncodeToString(sum[:])
|
|
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/index.yaml":
|
|
_, _ = fmt.Fprintf(w, `apiVersion: v1
|
|
entries:
|
|
demo:
|
|
- name: demo
|
|
version: 2.0.0
|
|
digest: %s
|
|
urls: [oci://ghcr.io/owner/demo:2.0.0]
|
|
- name: demo
|
|
version: 1.0.0
|
|
digest: %s
|
|
urls: [charts/demo-1.0.0.tgz]
|
|
`, strings.Repeat("b", 64), u.digest)
|
|
case "/charts/demo-1.0.0.tgz":
|
|
u.chartHits.Add(1)
|
|
_, _ = io.WriteString(w, u.chart)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(u.Close)
|
|
return u
|
|
}
|
|
|
|
func helmMirrorConfig(upstream *helmUpstream) *config.Config {
|
|
cfg := config.Default()
|
|
cfg.Upstream.Helm = map[string]string{"stable": upstream.URL}
|
|
return cfg
|
|
}
|
|
|
|
func TestMirrorHelmChartIsServedFromCache(t *testing.T) {
|
|
upstream := newHelmUpstream(t)
|
|
cfg := helmMirrorConfig(upstream)
|
|
|
|
progress, proxy := mirrorWithConfig(t, cfg, "pkg:helm/stable/demo@1.0.0")
|
|
|
|
if progress.Completed != 1 || upstream.chartHits.Load() != 1 {
|
|
t.Fatalf("completed = %d, chart hits = %d, errors = %v", progress.Completed, upstream.chartHits.Load(), progress.Errors)
|
|
}
|
|
|
|
// helm reads the index and then the chart it points to, both from the
|
|
// cache once the repository is down.
|
|
upstream.Close()
|
|
helm := handler.NewHelmHandler(proxy, "http://proxy.test", cfg.Upstream.Helm).Routes()
|
|
chartPath := "/stable/charts/" + upstream.digest + "/demo-1.0.0.tgz"
|
|
rec := httptest.NewRecorder()
|
|
helm.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/stable/index.yaml", nil))
|
|
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "http://proxy.test/helm"+chartPath) {
|
|
t.Fatalf("index from cache: status = %d, body %q; want 200 listing %s", rec.Code, rec.Body.String(), chartPath)
|
|
}
|
|
rec = httptest.NewRecorder()
|
|
helm.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, chartPath, nil))
|
|
if rec.Code != http.StatusOK || rec.Body.String() != upstream.chart {
|
|
t.Errorf("chart from cache: status = %d, body %q; want 200 and the mirrored chart", rec.Code, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
}
|
|
|
|
func TestMirrorHelmListsVersions(t *testing.T) {
|
|
upstream := newHelmUpstream(t)
|
|
|
|
progress, _ := mirrorWithConfig(t, helmMirrorConfig(upstream), "pkg:helm/stable/demo")
|
|
|
|
// 2.0.0 is in an OCI registry and refused.
|
|
if progress.Total != 2 || progress.Completed != 1 || progress.Failed != 1 || upstream.chartHits.Load() != 1 {
|
|
t.Errorf("total = %d, completed = %d, failed = %d, chart hits = %d, errors = %v; want both versions listed and 1.0.0 mirrored",
|
|
progress.Total, progress.Completed, progress.Failed, upstream.chartHits.Load(), progress.Errors)
|
|
}
|
|
}
|
|
|
|
func TestMirrorHelmRefusesDigestMismatch(t *testing.T) {
|
|
upstream := newHelmUpstream(t)
|
|
want := upstream.chart
|
|
upstream.chart = "tampered chart archive"
|
|
cfg := helmMirrorConfig(upstream)
|
|
|
|
progress, proxy := mirrorWithConfig(t, cfg, "pkg:helm/stable/demo@1.0.0")
|
|
|
|
if progress.Failed != 1 || progress.Completed != 0 {
|
|
t.Fatalf("completed = %d, failed = %d, errors = %v; want the chart refused", progress.Completed, progress.Failed, progress.Errors)
|
|
}
|
|
sum := sha256.Sum256([]byte(want))
|
|
upstream.Close()
|
|
rec := httptest.NewRecorder()
|
|
handler.NewHelmHandler(proxy, "http://proxy.test", cfg.Upstream.Helm).Routes().ServeHTTP(rec,
|
|
httptest.NewRequest(http.MethodGet, "/stable/charts/"+hex.EncodeToString(sum[:])+"/demo-1.0.0.tgz", nil))
|
|
if rec.Code == http.StatusOK {
|
|
t.Error("the tampered chart was cached")
|
|
}
|
|
}
|
|
|
|
func TestMirrorHelmRefusals(t *testing.T) {
|
|
tests := []struct {
|
|
name, purl, want string
|
|
}{
|
|
{"OCI chart", "pkg:helm/stable/demo@2.0.0", "pkg:oci/demo@2.0.0?repository_url=ghcr.io/owner/demo"},
|
|
{"unknown repository", "pkg:helm/other/demo@1.0.0", `unknown Helm repository "other": upstream.helm configures stable`},
|
|
{"no repository", "pkg:helm/demo@1.0.0", "pkg:helm/{repository}/{chart}@{version}"},
|
|
{"unknown version", "pkg:helm/stable/demo@3.0.0", "not in the stable index"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
upstream := newHelmUpstream(t)
|
|
|
|
progress, _ := mirrorWithConfig(t, helmMirrorConfig(upstream), tt.purl)
|
|
|
|
if progress.Failed != 1 || len(progress.Errors) != 1 || !strings.Contains(progress.Errors[0].Error, tt.want) {
|
|
t.Errorf("failed = %d, errors = %v; want an error mentioning %q", progress.Failed, progress.Errors, tt.want)
|
|
}
|
|
if upstream.chartHits.Load() != 0 {
|
|
t.Error("a chart was fetched")
|
|
}
|
|
})
|
|
}
|
|
}
|