mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
801 lines
33 KiB
Go
801 lines
33 KiB
Go
|
|
package handler
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"log/slog"
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/git-pkgs/registries/fetch"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
nsTestBlob = "layer bytes"
|
||
|
|
nsTestProxyURL = "http://proxy.example.test"
|
||
|
|
)
|
||
|
|
|
||
|
|
// nsTestRegistry is a fake OCI registry serving one repository below an
|
||
|
|
// optional path prefix. It records every request URI it receives.
|
||
|
|
type nsTestRegistry struct {
|
||
|
|
*httptest.Server
|
||
|
|
repository string
|
||
|
|
|
||
|
|
mu sync.Mutex
|
||
|
|
requests []string
|
||
|
|
}
|
||
|
|
|
||
|
|
func newNSTestRegistry(t *testing.T, repository, pathPrefix string) *nsTestRegistry {
|
||
|
|
t.Helper()
|
||
|
|
registry := &nsTestRegistry{repository: repository}
|
||
|
|
manifest := nsTestManifest()
|
||
|
|
blobDigest := "sha256:" + sha256Hex(nsTestBlob)
|
||
|
|
manifestDigest := "sha256:" + sha256Hex(manifest)
|
||
|
|
base := pathPrefix + "/v2/" + repository
|
||
|
|
registry.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
registry.mu.Lock()
|
||
|
|
registry.requests = append(registry.requests, r.URL.RequestURI())
|
||
|
|
registry.mu.Unlock()
|
||
|
|
|
||
|
|
switch r.URL.Path {
|
||
|
|
case base + "/manifests/latest", base + "/manifests/" + manifestDigest:
|
||
|
|
w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
||
|
|
w.Header().Set("Docker-Content-Digest", manifestDigest)
|
||
|
|
_, _ = io.WriteString(w, manifest)
|
||
|
|
case base + "/blobs/" + blobDigest:
|
||
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||
|
|
_, _ = io.WriteString(w, nsTestBlob)
|
||
|
|
case base + "/tags/list":
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
if r.URL.Query().Get("last") == "" {
|
||
|
|
w.Header().Set("Link", `<`+base+`/tags/list?last=1.0&n=1>; rel="next"`)
|
||
|
|
_, _ = io.WriteString(w, `{"name":"`+repository+`","tags":["1.0"]}`)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
_, _ = io.WriteString(w, `{"name":"`+repository+`","tags":["2.0"]}`)
|
||
|
|
default:
|
||
|
|
http.NotFound(w, r)
|
||
|
|
}
|
||
|
|
}))
|
||
|
|
t.Cleanup(registry.Close)
|
||
|
|
return registry
|
||
|
|
}
|
||
|
|
|
||
|
|
func nsTestManifest() string {
|
||
|
|
return `{"schemaVersion":2,"layers":[{"digest":"sha256:` + sha256Hex(nsTestBlob) + `"}]}`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *nsTestRegistry) host() string {
|
||
|
|
return strings.TrimPrefix(r.URL, "http://")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *nsTestRegistry) requestCount() int {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
return len(r.requests)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *nsTestRegistry) lastRequest() string {
|
||
|
|
r.mu.Lock()
|
||
|
|
defer r.mu.Unlock()
|
||
|
|
if len(r.requests) == 0 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return r.requests[len(r.requests)-1]
|
||
|
|
}
|
||
|
|
|
||
|
|
// newNSTestHandler builds a container handler the way the server does, with a
|
||
|
|
// real fetcher so blob downloads reach the fake registries. Warnings logged
|
||
|
|
// while building the handler are written to the returned buffer.
|
||
|
|
func newNSTestHandler(t *testing.T, defaultURL string, named map[string]string) (http.Handler, *ContainerHandler, *bytes.Buffer) {
|
||
|
|
t.Helper()
|
||
|
|
proxy, _, _, _ := setupTestProxy(t)
|
||
|
|
logs := &bytes.Buffer{}
|
||
|
|
proxy.Logger = slog.New(slog.NewTextHandler(logs, nil))
|
||
|
|
client := &http.Client{}
|
||
|
|
proxy.HTTPClient = client
|
||
|
|
proxy.MetadataTTL = time.Hour
|
||
|
|
fetcher := fetch.NewFetcher(fetch.WithHTTPClient(client), fetch.WithMaxRetries(0))
|
||
|
|
proxy.Fetcher = fetcher
|
||
|
|
t.Cleanup(func() { _ = fetcher.Close() })
|
||
|
|
h := NewContainerHandlerWithRegistry(proxy, nsTestProxyURL, defaultURL, named)
|
||
|
|
return http.StripPrefix("/v2", h.Routes()), h, logs
|
||
|
|
}
|
||
|
|
|
||
|
|
func serveNS(routes http.Handler, target string) *httptest.ResponseRecorder {
|
||
|
|
response := httptest.NewRecorder()
|
||
|
|
routes.ServeHTTP(response, httptest.NewRequest(http.MethodGet, target, nil))
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertNameUnknown(t *testing.T, response *httptest.ResponseRecorder) {
|
||
|
|
t.Helper()
|
||
|
|
if response.Code != http.StatusNotFound {
|
||
|
|
t.Fatalf("status = %d, want 404: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if !strings.Contains(response.Body.String(), `"NAME_UNKNOWN"`) {
|
||
|
|
t.Errorf("body = %s, want NAME_UNKNOWN error", response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceSelectsDefaultRegistry(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "library/nginx", "")
|
||
|
|
// The default registry is a custom oci_default, so its host is only
|
||
|
|
// resolvable when the index is built from the final registry URL.
|
||
|
|
routes, _, _ := newNSTestHandler(t, registry.URL, nil)
|
||
|
|
|
||
|
|
for _, namespace := range []string{"docker.io", "index.docker.io", "registry-1.docker.io", registry.host()} {
|
||
|
|
t.Run(namespace, func(t *testing.T) {
|
||
|
|
response := serveNS(routes, "/v2/library/nginx/manifests/latest?ns="+namespace)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status = %d, want 200: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if got, want := registry.lastRequest(), "/v2/library/nginx/manifests/latest"; got != want {
|
||
|
|
t.Errorf("upstream request = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceSelectsNamedRegistry(t *testing.T) {
|
||
|
|
fallback := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
named := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, fallback.URL, map[string]string{"ghcr": named.URL})
|
||
|
|
digest := "sha256:" + sha256Hex(nsTestBlob)
|
||
|
|
|
||
|
|
manifest := serveNS(routes, "/v2/owner/app/manifests/latest?ns="+named.host())
|
||
|
|
if manifest.Code != http.StatusOK {
|
||
|
|
t.Fatalf("manifest status = %d, want 200: %s", manifest.Code, manifest.Body.String())
|
||
|
|
}
|
||
|
|
blob := serveNS(routes, "/v2/owner/app/blobs/"+digest+"?ns="+named.host())
|
||
|
|
if blob.Code != http.StatusOK {
|
||
|
|
t.Fatalf("blob status = %d, want 200: %s", blob.Code, blob.Body.String())
|
||
|
|
}
|
||
|
|
if got := blob.Body.String(); got != nsTestBlob {
|
||
|
|
t.Errorf("blob body = %q, want %q", got, nsTestBlob)
|
||
|
|
}
|
||
|
|
if got := named.requestCount(); got != 2 {
|
||
|
|
t.Errorf("named registry requests = %d, want 2", got)
|
||
|
|
}
|
||
|
|
if got := fallback.requestCount(); got != 0 {
|
||
|
|
t.Errorf("default registry requests = %d, want 0", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceRejectsUnresolvableRequests(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
other := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, registry.URL, map[string]string{"ghcr": registry.URL, "quay": other.URL})
|
||
|
|
digest := "sha256:" + sha256Hex(nsTestBlob)
|
||
|
|
|
||
|
|
tests := map[string]string{
|
||
|
|
"unknown host manifest": "/v2/owner/app/manifests/latest?ns=quay.io",
|
||
|
|
"unknown host blob": "/v2/owner/app/blobs/" + digest + "?ns=quay.io",
|
||
|
|
"unknown host tags": "/v2/owner/app/tags/list?ns=quay.io",
|
||
|
|
"multiple ns values": "/v2/owner/app/manifests/latest?ns=docker.io&ns=" + registry.host(),
|
||
|
|
"prefix route with default host": "/v2/upstream/quay/owner/app/manifests/latest?ns=" + registry.host(),
|
||
|
|
"prefix route with other upstream": "/v2/upstream/ghcr/owner/app/blobs/" + digest + "?ns=" + other.host(),
|
||
|
|
"prefix route with unknown upstream": "/v2/upstream/nope/owner/app/manifests/latest?ns=" + registry.host(),
|
||
|
|
"prefix route without repository": "/v2/upstream/ghcr/manifests/latest?ns=" + registry.host(),
|
||
|
|
}
|
||
|
|
for name, target := range tests {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
assertNameUnknown(t, serveNS(routes, target))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
if got := registry.requestCount() + other.requestCount(); got != 0 {
|
||
|
|
t.Errorf("upstream requests = %d, want 0", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceAcceptsPrefixRouteForOwnHost(t *testing.T) {
|
||
|
|
// Per-registry containerd mirrors with override_path address the
|
||
|
|
// upstream/{name}/ prefix and still send ns. They must keep working and
|
||
|
|
// share the prefix route's cache entries.
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, "", map[string]string{"ghcr": registry.URL})
|
||
|
|
digest := "sha256:" + sha256Hex(nsTestBlob)
|
||
|
|
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/upstream/ghcr/owner/app/manifests/latest?ns=" + registry.host(),
|
||
|
|
"/v2/upstream/ghcr/owner/app/blobs/" + digest + "?ns=" + registry.host(),
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("%s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got, want := registry.lastRequest(), "/v2/owner/app/blobs/"+digest; got != want {
|
||
|
|
t.Errorf("upstream request = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
warmed := registry.requestCount()
|
||
|
|
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/upstream/ghcr/owner/app/manifests/latest",
|
||
|
|
"/v2/upstream/ghcr/owner/app/blobs/" + digest,
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("%s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != warmed {
|
||
|
|
t.Errorf("upstream requests after prefix pulls without ns = %d, want %d (cache hits)", got, warmed)
|
||
|
|
}
|
||
|
|
|
||
|
|
tags := serveNS(routes, "/v2/upstream/ghcr/owner/app/tags/list?n=1&ns="+registry.host())
|
||
|
|
if tags.Code != http.StatusOK {
|
||
|
|
t.Fatalf("tags status = %d: %s", tags.Code, tags.Body.String())
|
||
|
|
}
|
||
|
|
wantLink := `<` + nsTestProxyURL + `/v2/upstream/ghcr/owner/app/tags/list?last=1.0&n=1&ns=` + url.QueryEscape(registry.host()) + `>; rel="next"`
|
||
|
|
if got := tags.Header().Get("Link"); got != wantLink {
|
||
|
|
t.Errorf("Link = %q, want %q", got, wantLink)
|
||
|
|
}
|
||
|
|
if got, want := registry.lastRequest(), "/v2/owner/app/tags/list?n=1"; got != want {
|
||
|
|
t.Errorf("upstream tags request = %q, want %q (ns must not be forwarded)", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespacePrefixRouteAcceptsDockerHubAliases(t *testing.T) {
|
||
|
|
// upstream.oci.hub points at Docker Hub and a docker.io hosts.toml mirror
|
||
|
|
// with override_path addresses /v2/upstream/hub, so containerd sends
|
||
|
|
// ns=docker.io. The dialer stands in for registry-1.docker.io.
|
||
|
|
hub := newNSTestRegistry(t, "library/nginx", "")
|
||
|
|
other := newNSTestRegistry(t, "library/nginx", "")
|
||
|
|
routes, h, _ := newNSTestHandler(t, "", map[string]string{
|
||
|
|
"hub": "http://registry-1.docker.io",
|
||
|
|
"quay": other.URL,
|
||
|
|
})
|
||
|
|
transport := &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||
|
|
if addr != "registry-1.docker.io:80" {
|
||
|
|
return nil, fmt.Errorf("unexpected dial to %s", addr)
|
||
|
|
}
|
||
|
|
return (&net.Dialer{}).DialContext(ctx, network, hub.Listener.Addr().String())
|
||
|
|
}}
|
||
|
|
t.Cleanup(transport.CloseIdleConnections)
|
||
|
|
h.proxy.HTTPClient = &http.Client{Transport: transport}
|
||
|
|
|
||
|
|
for _, query := range []string{"?ns=docker.io", "?ns=index.docker.io", "?ns=registry-1.docker.io", "?ns=docker.io:443", ""} {
|
||
|
|
response := serveNS(routes, "/v2/upstream/hub/library/nginx/manifests/latest"+query)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("%q status = %d: %s", query, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := hub.requestCount(); got != 1 {
|
||
|
|
t.Errorf("hub requests = %d, want 1 (the rest are cache hits)", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A known host of another route still contradicts the prefix.
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/hub/library/nginx/manifests/latest?ns="+other.host()))
|
||
|
|
if got := other.requestCount(); got != 0 {
|
||
|
|
t.Errorf("other registry requests = %d, want 0", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Run("Docker Hub mirror as named upstream", func(t *testing.T) {
|
||
|
|
// The upstream is a Docker Hub mirror on some other host (mirror.gcr.io,
|
||
|
|
// an Artifactory remote); nodes still pull docker.io/... through
|
||
|
|
// /v2/upstream/hub, so ns says docker.io.
|
||
|
|
mirror := newNSTestRegistry(t, "library/nginx", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, "", map[string]string{"hub": mirror.URL})
|
||
|
|
|
||
|
|
response := serveNS(routes, "/v2/upstream/hub/library/nginx/manifests/latest?ns=docker.io")
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status = %d: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if got, want := mirror.lastRequest(), "/v2/library/nginx/manifests/latest"; got != want {
|
||
|
|
t.Errorf("upstream request = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespacePrefixRouteRejectsUnknownHosts(t *testing.T) {
|
||
|
|
// A _default wildcard mirror turns a pull of
|
||
|
|
// unconfigured.example/upstream/mirror/owner/app into exactly this
|
||
|
|
// request. Serving it would hand out owner/app from the mirror upstream
|
||
|
|
// instead of letting containerd fall back to unconfigured.example.
|
||
|
|
mirror := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, h, logs := newNSTestHandler(t, "", map[string]string{"mirror": mirror.URL})
|
||
|
|
|
||
|
|
for _, namespace := range []string{"unconfigured.example", "ghcr.io"} {
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/mirror/owner/app/manifests/latest?ns="+namespace))
|
||
|
|
}
|
||
|
|
if got := mirror.requestCount(); got != 0 {
|
||
|
|
t.Errorf("upstream requests = %d, want 0", got)
|
||
|
|
}
|
||
|
|
// containerd swallows the 404, so the refusal has to show up in the log.
|
||
|
|
for _, want := range []string{"upstream.oci_mirrors", "upstream=mirror", "ns=ghcr.io"} {
|
||
|
|
if !strings.Contains(logs.String(), want) {
|
||
|
|
t.Errorf("logs missing %q:\n%s", want, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// A node repeats the refused request for every pull; one line per
|
||
|
|
// upstream/ns pair is enough. An ns longer than any registry host is
|
||
|
|
// refused before it is logged or remembered.
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/mirror/owner/app/manifests/latest?ns=GHCR.io"))
|
||
|
|
long := strings.Repeat("a", 300) + ".example"
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/mirror/owner/app/manifests/latest?ns="+long))
|
||
|
|
if got := strings.Count(logs.String(), "upstream.oci_mirrors"); got != 2 {
|
||
|
|
t.Errorf("refusal warnings = %d, want 2 (one per upstream/ns pair):\n%s", got, logs.String())
|
||
|
|
}
|
||
|
|
if strings.Contains(logs.String(), "aaaa") {
|
||
|
|
t.Errorf("logs mention the overlong ns, want it refused silently:\n%s", logs.String())
|
||
|
|
}
|
||
|
|
if got := len(h.refusals); got != 2 {
|
||
|
|
t.Errorf("remembered refusals = %d, want 2", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceRefusalLogIsBounded(t *testing.T) {
|
||
|
|
mirror := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, h, logs := newNSTestHandler(t, "", map[string]string{"mirror": mirror.URL})
|
||
|
|
h.refusals = make(map[string]struct{}, maxReportedNamespaceRefusals)
|
||
|
|
for i := range maxReportedNamespaceRefusals {
|
||
|
|
h.refusals[fmt.Sprintf("mirror\x00filler-%d.example", i)] = struct{}{}
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, namespace := range []string{"filler-0.example", "first.example", "second.example"} {
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/mirror/owner/app/manifests/latest?ns="+namespace))
|
||
|
|
}
|
||
|
|
if got := strings.Count(logs.String(), "further refusals are not logged"); got != 1 {
|
||
|
|
t.Errorf("limit notices = %d, want 1:\n%s", got, logs.String())
|
||
|
|
}
|
||
|
|
if strings.Contains(logs.String(), "refusing ns") {
|
||
|
|
t.Errorf("refusal logged past the limit:\n%s", logs.String())
|
||
|
|
}
|
||
|
|
if got := len(h.refusals); got != maxReportedNamespaceRefusals {
|
||
|
|
t.Errorf("remembered refusals = %d, want %d", got, maxReportedNamespaceRefusals)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceMirroredRegistries(t *testing.T) {
|
||
|
|
// The upstream mirrors ghcr.io, and upstream.oci_mirrors says so. Nodes
|
||
|
|
// keep pulling ghcr.io/..., either through a ghcr.io hosts.toml pointing
|
||
|
|
// at /v2/upstream/ghcr or through the _default wildcard mirror.
|
||
|
|
tests := map[string]string{
|
||
|
|
"plain mirror host": "",
|
||
|
|
"artifactory remote": "/artifactory/api/docker/ghcr-remote",
|
||
|
|
}
|
||
|
|
for name, pathPrefix := range tests {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
mirror := newNSTestRegistry(t, "owner/app", pathPrefix)
|
||
|
|
routes, h, _ := newNSTestHandler(t, "", map[string]string{"ghcr": mirror.URL + pathPrefix})
|
||
|
|
h.SetMirroredRegistries(map[string][]string{"ghcr": {"ghcr.io"}})
|
||
|
|
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/upstream/ghcr/owner/app/manifests/latest?ns=ghcr.io",
|
||
|
|
"/v2/upstream/ghcr/owner/app/manifests/latest?ns=GHCR.io:443",
|
||
|
|
"/v2/owner/app/manifests/latest?ns=ghcr.io",
|
||
|
|
} {
|
||
|
|
before := mirror.requestCount()
|
||
|
|
response := serveNS(routes, target)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("GET %s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
// The manifest is cached after the first request, so only
|
||
|
|
// the first one may reach the upstream.
|
||
|
|
if before == 0 {
|
||
|
|
if got, want := mirror.lastRequest(), pathPrefix+"/v2/owner/app/manifests/latest"; got != want {
|
||
|
|
t.Errorf("upstream request = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := mirror.requestCount(); got != 1 {
|
||
|
|
t.Errorf("upstream requests = %d, want 1 (all routes share the cache)", got)
|
||
|
|
}
|
||
|
|
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/upstream/ghcr/owner/app/manifests/latest?ns=quay.io"))
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceMirrorHostsRankBelowConfiguredURLs(t *testing.T) {
|
||
|
|
proxy, _, _, _ := setupTestProxy(t)
|
||
|
|
logs := &bytes.Buffer{}
|
||
|
|
proxy.Logger = slog.New(slog.NewTextHandler(logs, nil))
|
||
|
|
h := NewContainerHandlerWithRegistry(proxy, nsTestProxyURL, "", map[string]string{
|
||
|
|
"ghcr": "https://ghcr.io",
|
||
|
|
"art": "https://artifactory.example/api/docker/ghcr-remote",
|
||
|
|
"other": "https://other.example",
|
||
|
|
})
|
||
|
|
h.SetMirroredRegistries(map[string][]string{"art": {"ghcr.io"}, "missing": {"quay.io"}})
|
||
|
|
|
||
|
|
if got := h.namespaces["ghcr.io"]; got != "ghcr" {
|
||
|
|
t.Errorf("index[ghcr.io] = %q, want ghcr (configured URL wins over a mirror entry)", got)
|
||
|
|
}
|
||
|
|
if !h.namespaceNamesPrefixUpstream("ghcr.io", "upstream/art/owner/app") {
|
||
|
|
t.Error("ns ghcr.io refused on upstream/art/, want accepted as its mirrored registry")
|
||
|
|
}
|
||
|
|
if h.namespaceNamesPrefixUpstream("ghcr.io", "upstream/other/owner/app") {
|
||
|
|
t.Error("ns ghcr.io accepted on upstream/other/, want refused: only art lists it")
|
||
|
|
}
|
||
|
|
if _, ok := h.namespaces["quay.io"]; ok {
|
||
|
|
t.Error("index has quay.io from a mirror entry without upstream")
|
||
|
|
}
|
||
|
|
for _, want := range []string{"ghcr.io=ghcr", "upstream=missing"} {
|
||
|
|
if !strings.Contains(logs.String(), want) {
|
||
|
|
t.Errorf("logs missing %q:\n%s", want, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceSharesCacheWithOtherRoutes(t *testing.T) {
|
||
|
|
digest := "sha256:" + sha256Hex(nsTestBlob)
|
||
|
|
manifestDigest := "sha256:" + sha256Hex(nsTestManifest())
|
||
|
|
|
||
|
|
t.Run("named registry", func(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, "", map[string]string{"ghcr": registry.URL})
|
||
|
|
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/upstream/ghcr/owner/app/blobs/" + digest,
|
||
|
|
"/v2/upstream/ghcr/owner/app/manifests/" + manifestDigest,
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("warm %s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
warmed := registry.requestCount()
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/owner/app/blobs/" + digest + "?ns=" + registry.host(),
|
||
|
|
"/v2/owner/app/manifests/" + manifestDigest + "?ns=" + registry.host(),
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("ns %s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != warmed {
|
||
|
|
t.Errorf("upstream requests after ns pulls = %d, want %d (cache hits)", got, warmed)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("default registry", func(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "library/nginx", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, registry.URL, nil)
|
||
|
|
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/library/nginx/blobs/" + digest + "?ns=docker.io",
|
||
|
|
"/v2/library/nginx/manifests/" + manifestDigest + "?ns=docker.io",
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("warm %s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
warmed := registry.requestCount()
|
||
|
|
for _, target := range []string{
|
||
|
|
"/v2/library/nginx/blobs/" + digest,
|
||
|
|
"/v2/library/nginx/manifests/" + manifestDigest,
|
||
|
|
} {
|
||
|
|
if response := serveNS(routes, target); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("unprefixed %s status = %d: %s", target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != warmed {
|
||
|
|
t.Errorf("upstream requests after unprefixed pulls = %d, want %d (cache hits)", got, warmed)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceTagsList(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, _, _ := newNSTestHandler(t, "", map[string]string{"ghcr": registry.URL})
|
||
|
|
|
||
|
|
// The prefix route fills the shared cache entry first.
|
||
|
|
prefixed := serveNS(routes, "/v2/upstream/ghcr/owner/app/tags/list?n=1")
|
||
|
|
if prefixed.Code != http.StatusOK {
|
||
|
|
t.Fatalf("prefixed status = %d: %s", prefixed.Code, prefixed.Body.String())
|
||
|
|
}
|
||
|
|
const wantPrefixedLink = `<` + nsTestProxyURL + `/v2/upstream/ghcr/owner/app/tags/list?last=1.0&n=1>; rel="next"`
|
||
|
|
if got := prefixed.Header().Get("Link"); got != wantPrefixedLink {
|
||
|
|
t.Errorf("prefixed Link = %q, want %q", got, wantPrefixedLink)
|
||
|
|
}
|
||
|
|
|
||
|
|
namespaced := serveNS(routes, "/v2/owner/app/tags/list?n=1&ns="+registry.host())
|
||
|
|
if namespaced.Code != http.StatusOK {
|
||
|
|
t.Fatalf("ns status = %d: %s", namespaced.Code, namespaced.Body.String())
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != 1 {
|
||
|
|
t.Errorf("upstream requests = %d, want 1 (ns request served from the shared cache)", got)
|
||
|
|
}
|
||
|
|
wantNSLink := `<` + nsTestProxyURL + `/v2/owner/app/tags/list?last=1.0&n=1&ns=` + url.QueryEscape(registry.host()) + `>; rel="next"`
|
||
|
|
link := namespaced.Header().Get("Link")
|
||
|
|
if link != wantNSLink {
|
||
|
|
t.Fatalf("ns Link = %q, want %q", link, wantNSLink)
|
||
|
|
}
|
||
|
|
|
||
|
|
next := serveNS(routes, strings.TrimPrefix(strings.SplitN(link, ">", 2)[0], "<"))
|
||
|
|
if next.Code != http.StatusOK {
|
||
|
|
t.Fatalf("next page status = %d: %s", next.Code, next.Body.String())
|
||
|
|
}
|
||
|
|
if got, want := next.Body.String(), `{"name":"owner/app","tags":["2.0"]}`; got != want {
|
||
|
|
t.Errorf("next page body = %q, want %q", got, want)
|
||
|
|
}
|
||
|
|
if got, want := registry.lastRequest(), "/v2/owner/app/tags/list?last=1.0&n=1"; got != want {
|
||
|
|
t.Errorf("upstream request = %q, want %q (ns must not be forwarded)", got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceDefaultBypassesRepositoryPrefixRoutes(t *testing.T) {
|
||
|
|
hub := newNSTestRegistry(t, "homebrew/core/jq", "")
|
||
|
|
brew := newNSTestRegistry(t, "homebrew/core/jq", "")
|
||
|
|
routes, h, _ := newNSTestHandler(t, hub.URL, nil)
|
||
|
|
RegisterHomebrewArtifacts(h, brew.URL)
|
||
|
|
|
||
|
|
if response := serveNS(routes, "/v2/homebrew/core/jq/manifests/latest?ns=docker.io"); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("ns status = %d: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if hub.requestCount() != 1 || brew.requestCount() != 0 {
|
||
|
|
t.Errorf("ns request reached hub=%d brew=%d, want hub=1 brew=0", hub.requestCount(), brew.requestCount())
|
||
|
|
}
|
||
|
|
|
||
|
|
if response := serveNS(routes, "/v2/homebrew/core/jq/manifests/latest"); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("unprefixed status = %d: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if hub.requestCount() != 1 || brew.requestCount() != 1 {
|
||
|
|
t.Errorf("unprefixed request reached hub=%d brew=%d, want hub=1 brew=1", hub.requestCount(), brew.requestCount())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceMatchesConfiguredHosts(t *testing.T) {
|
||
|
|
// Each pair is a configured registry URL and an ns value containerd sends
|
||
|
|
// for an image reference on that registry. The scheme-default port may be
|
||
|
|
// spelled out or left out on either side; any other port must match.
|
||
|
|
matches := []struct {
|
||
|
|
configured string
|
||
|
|
namespace string
|
||
|
|
}{
|
||
|
|
{"https://GHCR.io", "ghcr.io"},
|
||
|
|
{"https://ghcr.io/", "GHCR.IO"},
|
||
|
|
{"http://[fd00::1]:5000", "[fd00::1]:5000"},
|
||
|
|
{"https://[fd00::1]", "[fd00::1]"},
|
||
|
|
{"https://[fd00::1]", "[fd00::1]:443"},
|
||
|
|
{"https://reg.example:80", "reg.example:80"},
|
||
|
|
{"https://reg.example:443", "reg.example"},
|
||
|
|
{"https://reg.example", "reg.example:443"},
|
||
|
|
{"http://reg.example:80", "reg.example"},
|
||
|
|
{"http://reg.example", "reg.example:80"},
|
||
|
|
{"http://reg.example:5000", "reg.example:5000"},
|
||
|
|
}
|
||
|
|
for _, tt := range matches {
|
||
|
|
t.Run(tt.configured+" "+tt.namespace, func(t *testing.T) {
|
||
|
|
h := NewContainerHandlerWithRegistry(nil, nsTestProxyURL, "", map[string]string{"lab": tt.configured})
|
||
|
|
registryURL, upstreamName, cacheName, ok := h.registryForNamespace(tt.namespace, "owner/app")
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("ns %q did not resolve for upstream %q", tt.namespace, tt.configured)
|
||
|
|
}
|
||
|
|
if registryURL != strings.TrimSuffix(tt.configured, "/") || upstreamName != "owner/app" || cacheName != "upstream/lab/owner/app" {
|
||
|
|
t.Errorf("route = (%q, %q, %q), want (%q, owner/app, upstream/lab/owner/app)",
|
||
|
|
registryURL, upstreamName, cacheName, tt.configured)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
mismatches := []struct {
|
||
|
|
configured string
|
||
|
|
namespace string
|
||
|
|
}{
|
||
|
|
{"http://reg.example:5000", "reg.example:5001"},
|
||
|
|
{"https://reg.example:80", "reg.example"},
|
||
|
|
{"https://reg.example", "reg.example:80"},
|
||
|
|
{"http://reg.example:443", "reg.example"},
|
||
|
|
}
|
||
|
|
for _, tt := range mismatches {
|
||
|
|
t.Run("mismatch "+tt.configured+" "+tt.namespace, func(t *testing.T) {
|
||
|
|
h := NewContainerHandlerWithRegistry(nil, nsTestProxyURL, "", map[string]string{"lab": tt.configured})
|
||
|
|
if _, _, _, ok := h.registryForNamespace(tt.namespace, "owner/app"); ok {
|
||
|
|
t.Errorf("ns %q resolved for upstream %q, want no match", tt.namespace, tt.configured)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceKeepsNonDefaultPorts(t *testing.T) {
|
||
|
|
// Two registries on one host that differ only in the port, one of them on
|
||
|
|
// the scheme-default port. Tests cannot listen on ports 80 or 443, so the
|
||
|
|
// dialer maps those addresses to the fake registries.
|
||
|
|
onDefaultPort := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
onOtherPort := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, h, _ := newNSTestHandler(t, "", map[string]string{
|
||
|
|
"std": "http://registry.example",
|
||
|
|
"alt": "http://registry.example:443",
|
||
|
|
})
|
||
|
|
endpoints := map[string]string{
|
||
|
|
"registry.example:80": onDefaultPort.Listener.Addr().String(),
|
||
|
|
"registry.example:443": onOtherPort.Listener.Addr().String(),
|
||
|
|
}
|
||
|
|
transport := &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||
|
|
target, ok := endpoints[addr]
|
||
|
|
if !ok {
|
||
|
|
return nil, fmt.Errorf("unexpected dial to %s", addr)
|
||
|
|
}
|
||
|
|
return (&net.Dialer{}).DialContext(ctx, network, target)
|
||
|
|
}}
|
||
|
|
t.Cleanup(transport.CloseIdleConnections)
|
||
|
|
h.proxy.HTTPClient = &http.Client{Transport: transport}
|
||
|
|
|
||
|
|
// Every request below misses the cache: a different registry or a
|
||
|
|
// different endpoint than the request before it.
|
||
|
|
tests := []struct {
|
||
|
|
target string
|
||
|
|
wantDefault int
|
||
|
|
wantOther int
|
||
|
|
}{
|
||
|
|
{"/v2/owner/app/manifests/latest?ns=registry.example:80", 1, 0},
|
||
|
|
{"/v2/owner/app/manifests/latest?ns=registry.example:443", 1, 1},
|
||
|
|
{"/v2/owner/app/tags/list?ns=registry.example", 2, 1},
|
||
|
|
}
|
||
|
|
for _, tt := range tests {
|
||
|
|
response := serveNS(routes, tt.target)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("%s status = %d: %s", tt.target, response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if onDefaultPort.requestCount() != tt.wantDefault || onOtherPort.requestCount() != tt.wantOther {
|
||
|
|
t.Errorf("after %s: requests default-port=%d other-port=%d, want %d/%d",
|
||
|
|
tt.target, onDefaultPort.requestCount(), onOtherPort.requestCount(), tt.wantDefault, tt.wantOther)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceHostCollisions(t *testing.T) {
|
||
|
|
proxy, _, _, _ := setupTestProxy(t)
|
||
|
|
logs := &bytes.Buffer{}
|
||
|
|
proxy.Logger = slog.New(slog.NewTextHandler(logs, nil))
|
||
|
|
h := NewContainerHandlerWithRegistry(proxy, nsTestProxyURL, "https://mirror.example", map[string]string{
|
||
|
|
"zeta": "https://shared.example",
|
||
|
|
"alpha": "https://Shared.example:443",
|
||
|
|
"mirror": "https://mirror.example",
|
||
|
|
"hub": "https://registry-1.docker.io",
|
||
|
|
})
|
||
|
|
|
||
|
|
tests := map[string]string{
|
||
|
|
"shared.example": "https://Shared.example:443",
|
||
|
|
"mirror.example": "https://mirror.example",
|
||
|
|
"registry-1.docker.io": "https://mirror.example",
|
||
|
|
}
|
||
|
|
for namespace, want := range tests {
|
||
|
|
registryURL, _, cacheName, ok := h.registryForNamespace(namespace, "owner/app")
|
||
|
|
if !ok || registryURL != want {
|
||
|
|
t.Errorf("ns %q resolved to (%q, %v), want %q", namespace, registryURL, ok, want)
|
||
|
|
}
|
||
|
|
if namespace == "shared.example" && cacheName != "upstream/alpha/owner/app" {
|
||
|
|
t.Errorf("ns %q cache name = %q, want upstream/alpha/owner/app", namespace, cacheName)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, upstream := range []string{"upstream=zeta", "upstream=mirror", "upstream=hub"} {
|
||
|
|
if !strings.Contains(logs.String(), upstream) {
|
||
|
|
t.Errorf("missing collision warning for %s in logs:\n%s", upstream, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceCollisionWarningNamesEachOwner(t *testing.T) {
|
||
|
|
// r's two keys end up with two different owners; the warning has to
|
||
|
|
// name both, or an admin fixing one entry misses the other.
|
||
|
|
proxy, _, _, _ := setupTestProxy(t)
|
||
|
|
logs := &bytes.Buffer{}
|
||
|
|
proxy.Logger = slog.New(slog.NewTextHandler(logs, nil))
|
||
|
|
h := NewContainerHandlerWithRegistry(proxy, nsTestProxyURL, "", map[string]string{
|
||
|
|
"p": "http://h.example",
|
||
|
|
"q": "http://h.example:443",
|
||
|
|
"r": "https://h.example",
|
||
|
|
})
|
||
|
|
|
||
|
|
for namespace, want := range map[string]string{"h.example": "p", "h.example:80": "p", "h.example:443": "q"} {
|
||
|
|
if got := h.namespaces[namespace]; got != want {
|
||
|
|
t.Errorf("index[%q] = %q, want %q", namespace, got, want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, want := range []string{"upstream=r", "h.example=p", "h.example:443=q"} {
|
||
|
|
if !strings.Contains(logs.String(), want) {
|
||
|
|
t.Errorf("logs missing %q:\n%s", want, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceSkipsRegistryURLsWithPath(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "/artifactory/api/docker/remote")
|
||
|
|
routes, _, logs := newNSTestHandler(t, "", map[string]string{
|
||
|
|
"art": registry.URL + "/artifactory/api/docker/remote",
|
||
|
|
})
|
||
|
|
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/owner/app/manifests/latest?ns="+registry.host()))
|
||
|
|
if got := registry.requestCount(); got != 0 {
|
||
|
|
t.Errorf("upstream requests via ns = %d, want 0", got)
|
||
|
|
}
|
||
|
|
if !strings.Contains(logs.String(), "upstream=art") {
|
||
|
|
t.Errorf("missing warning for path-prefixed upstream in logs:\n%s", logs.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
// Per-registry mirrors with override_path reach it with ns attached.
|
||
|
|
if response := serveNS(routes, "/v2/upstream/art/owner/app/manifests/latest?ns="+registry.host()); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("prefix route with ns status = %d, want 200: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != 1 {
|
||
|
|
t.Errorf("upstream requests via prefix route with ns = %d, want 1", got)
|
||
|
|
}
|
||
|
|
if response := serveNS(routes, "/v2/upstream/art/owner/app/manifests/latest"); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("prefix route status = %d, want 200: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Run("default registry", func(t *testing.T) {
|
||
|
|
mirror := newNSTestRegistry(t, "library/nginx", "/hub")
|
||
|
|
routes, _, logs := newNSTestHandler(t, mirror.URL+"/hub", nil)
|
||
|
|
|
||
|
|
assertNameUnknown(t, serveNS(routes, "/v2/library/nginx/manifests/latest?ns="+mirror.host()))
|
||
|
|
if got := mirror.requestCount(); got != 0 {
|
||
|
|
t.Errorf("upstream requests via ns host = %d, want 0", got)
|
||
|
|
}
|
||
|
|
if response := serveNS(routes, "/v2/library/nginx/manifests/latest?ns=docker.io"); response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("ns=docker.io status = %d, want 200: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if !strings.Contains(logs.String(), "default OCI registry") {
|
||
|
|
t.Errorf("missing warning for path-prefixed default registry in logs:\n%s", logs.String())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_TagsListIgnoresLegacyCacheRows(t *testing.T) {
|
||
|
|
registry := newNSTestRegistry(t, "owner/app", "")
|
||
|
|
routes, h, _ := newNSTestHandler(t, "", map[string]string{"ghcr": registry.URL})
|
||
|
|
|
||
|
|
// Rows written before the raw-link format hold a Link already rewritten
|
||
|
|
// for the route that filled them. Seed one under the legacy identity.
|
||
|
|
query := url.Values{"n": {"1"}}
|
||
|
|
legacySum := sha256.Sum256([]byte(registry.URL + "\x00owner/app\x00" + query.Encode()))
|
||
|
|
legacyKey := hex.EncodeToString(legacySum[:])
|
||
|
|
if legacyKey == h.containerTagsCacheKey(registry.URL, "owner/app", query) {
|
||
|
|
t.Fatal("legacy and current tag-list cache keys are equal")
|
||
|
|
}
|
||
|
|
legacy := &cachedContainerTags{
|
||
|
|
body: []byte(`{"name":"owner/app","tags":["legacy"]}`),
|
||
|
|
contentType: contentTypeJSON,
|
||
|
|
link: `<` + nsTestProxyURL + `/v2/upstream/ghcr/owner/app/tags/list?last=legacy&n=1>; rel="next"`,
|
||
|
|
fetchedAt: time.Now(),
|
||
|
|
}
|
||
|
|
if err := h.storeContainerTags(context.Background(), legacyKey, legacy); err != nil {
|
||
|
|
t.Fatalf("store legacy tag list: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
response := serveNS(routes, "/v2/owner/app/tags/list?n=1&ns="+registry.host())
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status = %d: %s", response.Code, response.Body.String())
|
||
|
|
}
|
||
|
|
if got, want := response.Body.String(), `{"name":"owner/app","tags":["1.0"]}`; got != want {
|
||
|
|
t.Errorf("body = %q, want %q (legacy row must not be served)", got, want)
|
||
|
|
}
|
||
|
|
if got := registry.requestCount(); got != 1 {
|
||
|
|
t.Errorf("upstream requests = %d, want 1 (legacy row must not be served)", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContainerHandler_NamespaceWarningsRedactCredentials(t *testing.T) {
|
||
|
|
proxy, _, _, _ := setupTestProxy(t)
|
||
|
|
logs := &bytes.Buffer{}
|
||
|
|
proxy.Logger = slog.New(slog.NewTextHandler(logs, nil))
|
||
|
|
NewContainerHandlerWithRegistry(proxy, nsTestProxyURL, "https://svc:s3cret@mirror.example/hub", map[string]string{
|
||
|
|
"art": "https://bot:hunter2@art.example/artifactory/api/docker/remote",
|
||
|
|
})
|
||
|
|
|
||
|
|
for _, secret := range []string{"s3cret", "hunter2"} {
|
||
|
|
if strings.Contains(logs.String(), secret) {
|
||
|
|
t.Errorf("logs contain credential %q:\n%s", secret, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for _, want := range []string{
|
||
|
|
"svc:xxxxx@mirror.example/hub",
|
||
|
|
"bot:xxxxx@art.example/artifactory",
|
||
|
|
"Docker Hub aliases still select it",
|
||
|
|
} {
|
||
|
|
if !strings.Contains(logs.String(), want) {
|
||
|
|
t.Errorf("logs missing %q:\n%s", want, logs.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|