|
|
|
|
@@ -1,6 +1,57 @@
|
|
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import "testing"
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"hubproxy/config"
|
|
|
|
|
"hubproxy/utils"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type zeroReader struct{}
|
|
|
|
|
|
|
|
|
|
func (zeroReader) Read(p []byte) (int, error) {
|
|
|
|
|
for i := range p {
|
|
|
|
|
p[i] = 0
|
|
|
|
|
}
|
|
|
|
|
return len(p), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type discardResponseWriter struct {
|
|
|
|
|
header http.Header
|
|
|
|
|
status int
|
|
|
|
|
bytes int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newDiscardResponseWriter() *discardResponseWriter {
|
|
|
|
|
return &discardResponseWriter{header: make(http.Header)}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *discardResponseWriter) Header() http.Header {
|
|
|
|
|
return w.header
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *discardResponseWriter) WriteHeader(status int) {
|
|
|
|
|
w.status = status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *discardResponseWriter) Write(p []byte) (int, error) {
|
|
|
|
|
if w.status == 0 {
|
|
|
|
|
w.status = http.StatusOK
|
|
|
|
|
}
|
|
|
|
|
w.bytes += int64(len(p))
|
|
|
|
|
return len(p), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParseRegistryPath(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
@@ -28,3 +79,859 @@ func TestParseRegistryPathInvalid(t *testing.T) {
|
|
|
|
|
t.Fatalf("invalid path parsed as %q %q %q", image, apiType, reference)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type testEnv interface {
|
|
|
|
|
Helper()
|
|
|
|
|
TempDir() string
|
|
|
|
|
Setenv(string, string)
|
|
|
|
|
Fatal(...interface{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func initDockerProxyTest(t testEnv, configBody string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
path := filepath.Join(t.TempDir(), "config.toml")
|
|
|
|
|
if err := os.WriteFile(path, []byte(configBody), 0644); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Setenv("CONFIG_PATH", path)
|
|
|
|
|
if err := config.LoadConfig(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
utils.InitHTTPClients()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRewriteAuthChallengePreservesScopeAndUsesProxyRealm(t *testing.T) {
|
|
|
|
|
target := registryTarget{
|
|
|
|
|
Name: "ghcr.io",
|
|
|
|
|
AuthService: "ghcr.io",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got := rewriteAuthChallenge(
|
|
|
|
|
`Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:owner/image:pull"`,
|
|
|
|
|
target,
|
|
|
|
|
"https://proxy.example.com",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
want := `Bearer realm="https://proxy.example.com/token/ghcr.io",service="ghcr.io",scope="repository:owner/image:pull"`
|
|
|
|
|
if got != want {
|
|
|
|
|
t.Fatalf("challenge = %q, want %q", got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildAuthURLForDockerHubAddsLibraryScopeAndService(t *testing.T) {
|
|
|
|
|
got, err := buildAuthURL(
|
|
|
|
|
defaultRegistryTarget(),
|
|
|
|
|
"service=ignored&scope=repository%3Aalpine%3Apull&client_id=docker",
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.HasPrefix(got, dockerHubAuthRealm+"?") {
|
|
|
|
|
t.Fatalf("auth URL = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, "service=registry.docker.io") {
|
|
|
|
|
t.Fatalf("auth URL missing service: %q", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, "scope=repository%3Alibrary%2Falpine%3Apull") {
|
|
|
|
|
t.Fatalf("auth URL missing normalized scope: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTokenTargetIsInferredFromPathBasedRegistryScope(t *testing.T) {
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."ghcr.io"]
|
|
|
|
|
upstream = "ghcr.io"
|
|
|
|
|
authHost = "ghcr.io/token"
|
|
|
|
|
authType = "github"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository:ghcr.io/jeessy2/ddns-go:pull&service=registry.docker.io", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
|
c.Request = req
|
|
|
|
|
c.Params = gin.Params{{Key: "path", Value: "/docker.io"}}
|
|
|
|
|
|
|
|
|
|
target, ok := resolveTokenTarget(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("resolveTokenTarget returned false")
|
|
|
|
|
}
|
|
|
|
|
if target.Name != "ghcr.io" {
|
|
|
|
|
t.Fatalf("target.Name = %q, want ghcr.io", target.Name)
|
|
|
|
|
}
|
|
|
|
|
if target.AuthService != "ghcr.io" {
|
|
|
|
|
t.Fatalf("AuthService = %q, want ghcr.io", target.AuthService)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildAuthURLStripsPathBasedRegistryPrefixForGHCR(t *testing.T) {
|
|
|
|
|
target := registryTarget{
|
|
|
|
|
Name: "ghcr.io",
|
|
|
|
|
AuthRealm: "https://ghcr.io/token",
|
|
|
|
|
AuthService: "ghcr.io",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
got, err := buildAuthURL(
|
|
|
|
|
target,
|
|
|
|
|
"scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io",
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(got, "service=ghcr.io") {
|
|
|
|
|
t.Fatalf("auth URL missing ghcr service: %q", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, "scope=repository%3Ajeessy2%2Fddns-go%3Apull") {
|
|
|
|
|
t.Fatalf("auth URL missing stripped scope: %q", got)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(got, "registry.docker.io") {
|
|
|
|
|
t.Fatalf("auth URL leaked Docker Hub service: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDockerIODefaultTargetUsesBuiltInWhenUnconfigured(t *testing.T) {
|
|
|
|
|
initDockerProxyTest(t, "")
|
|
|
|
|
|
|
|
|
|
target := defaultRegistryTarget()
|
|
|
|
|
if target.Upstream != dockerHubUpstream {
|
|
|
|
|
t.Fatalf("Upstream = %q, want %q", target.Upstream, dockerHubUpstream)
|
|
|
|
|
}
|
|
|
|
|
if target.AuthRealm != dockerHubAuthRealm {
|
|
|
|
|
t.Fatalf("AuthRealm = %q, want %q", target.AuthRealm, dockerHubAuthRealm)
|
|
|
|
|
}
|
|
|
|
|
if !target.AutoLibraryPrefix {
|
|
|
|
|
t.Fatal("AutoLibraryPrefix = false, want true")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDockerIODefaultTargetCanBeOverriddenByConfig(t *testing.T) {
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."docker.io"]
|
|
|
|
|
upstream = "mirror.local"
|
|
|
|
|
authHost = "auth.mirror.local/token"
|
|
|
|
|
authType = "docker"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
target := defaultRegistryTarget()
|
|
|
|
|
if target.Upstream != "https://mirror.local" {
|
|
|
|
|
t.Fatalf("Upstream = %q, want custom mirror", target.Upstream)
|
|
|
|
|
}
|
|
|
|
|
if target.AuthRealm != "https://auth.mirror.local/token" {
|
|
|
|
|
t.Fatalf("AuthRealm = %q, want custom auth realm", target.AuthRealm)
|
|
|
|
|
}
|
|
|
|
|
if target.AuthService != dockerHubAuthService {
|
|
|
|
|
t.Fatalf("AuthService = %q, want %q", target.AuthService, dockerHubAuthService)
|
|
|
|
|
}
|
|
|
|
|
if !target.AutoLibraryPrefix {
|
|
|
|
|
t.Fatal("AutoLibraryPrefix = false, want true")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDockerIODefaultTargetIgnoresDisabledOverride(t *testing.T) {
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."docker.io"]
|
|
|
|
|
upstream = "mirror.local"
|
|
|
|
|
authHost = "auth.mirror.local/token"
|
|
|
|
|
authType = "docker"
|
|
|
|
|
enabled = false
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
target := defaultRegistryTarget()
|
|
|
|
|
if target.Upstream != dockerHubUpstream {
|
|
|
|
|
t.Fatalf("Upstream = %q, want built-in %q", target.Upstream, dockerHubUpstream)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryTransparentlyForwardsAuthAndRewritesChallenge(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
|
|
|
|
t.Fatalf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.Header.Get("Authorization"); got != "Bearer client-token" {
|
|
|
|
|
t.Fatalf("Authorization = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.Header.Get("Accept"); got != "application/vnd.docker.distribution.manifest.v2+json" {
|
|
|
|
|
t.Fatalf("Accept = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.Header.Get("Range"); got != "bytes=0-99" {
|
|
|
|
|
t.Fatalf("Range = %q", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="https://upstream.example/token",service="upstream.example",scope="repository:team/app:pull"`)
|
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Host = "proxy.example.com"
|
|
|
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
|
|
|
req.Header.Set("Authorization", "Bearer client-token")
|
|
|
|
|
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
req.Header.Set("Range", "bytes=0-99")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
|
|
|
t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wantChallenge := `Bearer realm="https://proxy.example.com/token/test.local",service="` + strings.TrimPrefix(upstream.URL, "http://") + `",scope="repository:team/app:pull"`
|
|
|
|
|
if got := w.Header().Get("WWW-Authenticate"); got != wantChallenge {
|
|
|
|
|
t.Fatalf("WWW-Authenticate = %q, want %q", got, wantChallenge)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDockerV2BaseProxiesUpstreamChallenge(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/v2/" {
|
|
|
|
|
t.Fatalf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Bearer realm="https://registry.example/token",service="registry.example"`)
|
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."docker.io"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.example/token"
|
|
|
|
|
authType = "docker"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/", nil)
|
|
|
|
|
req.Host = "hub.example.com"
|
|
|
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
|
|
|
t.Fatalf("status = %d, want 401; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wantChallenge := `Bearer realm="https://hub.example.com/token/docker.io",service="registry.docker.io"`
|
|
|
|
|
if got := w.Header().Get("WWW-Authenticate"); got != wantChallenge {
|
|
|
|
|
t.Fatalf("WWW-Authenticate = %q, want %q", got, wantChallenge)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerAuthForwardsBasicCredentials(t *testing.T) {
|
|
|
|
|
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if got := r.Header.Get("Authorization"); got != "Basic dXNlcjpwYXNz" {
|
|
|
|
|
t.Fatalf("Authorization = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("service"); got != "127.0.0.1" {
|
|
|
|
|
t.Fatalf("service = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("scope"); got != "repository:team/app:pull" {
|
|
|
|
|
t.Fatalf("scope = %q", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write([]byte(`{"token":"secret","expires_in":3600}`))
|
|
|
|
|
}))
|
|
|
|
|
defer authServer.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "https://127.0.0.1"
|
|
|
|
|
authHost = "`+authServer.URL+`"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/token/*path", ProxyDockerAuthGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.String(); !strings.Contains(got, `"token":"secret"`) {
|
|
|
|
|
t.Fatalf("body = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerAuthRoutesPathBasedGHCRScopeToGHCRAuth(t *testing.T) {
|
|
|
|
|
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if got := r.URL.Query().Get("service"); got != "ghcr.io" {
|
|
|
|
|
t.Fatalf("service = %q, want ghcr.io", got)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("scope"); got != "repository:jeessy2/ddns-go:pull" {
|
|
|
|
|
t.Fatalf("scope = %q, want repository:jeessy2/ddns-go:pull", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = w.Write([]byte(`{"token":"ghcr-token","expires_in":3600}`))
|
|
|
|
|
}))
|
|
|
|
|
defer authServer.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."ghcr.io"]
|
|
|
|
|
upstream = "ghcr.io"
|
|
|
|
|
authHost = "`+authServer.URL+`"
|
|
|
|
|
authType = "github"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/token/*path", ProxyDockerAuthGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/docker.io?scope=repository%3Aghcr.io%2Fjeessy2%2Fddns-go%3Apull&service=registry.docker.io", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.String(); !strings.Contains(got, `"token":"ghcr-token"`) {
|
|
|
|
|
t.Fatalf("body = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDockerHubShortNameIsProxiedWithLibraryPrefix(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/v2/library/nginx/manifests/latest" {
|
|
|
|
|
t.Fatalf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
w.Header().Set("Docker-Content-Digest", "sha256:abc")
|
|
|
|
|
_, _ = w.Write([]byte(`{"schemaVersion":2}`))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
target := defaultRegistryTarget()
|
|
|
|
|
target.Upstream = upstream.URL
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/nginx/manifests/latest", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
|
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
|
c.Request = req
|
|
|
|
|
proxyRegistryHTTP(c, target, "/v2/library/nginx/manifests/latest")
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Docker-Content-Digest"); got != "sha256:abc" {
|
|
|
|
|
t.Fatalf("Docker-Content-Digest = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryHeadReturnsHeadersWithoutBody(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.Method != http.MethodHead {
|
|
|
|
|
t.Fatalf("method = %s, want HEAD", r.Method)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Length", "123")
|
|
|
|
|
w.Header().Set("Docker-Content-Digest", "sha256:head")
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodHead, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200", w.Code)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Docker-Content-Digest"); got != "sha256:head" {
|
|
|
|
|
t.Fatalf("Docker-Content-Digest = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if body := w.Body.String(); body != "" {
|
|
|
|
|
t.Fatalf("HEAD body = %q, want empty", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryStreamsBlobAndSkipsHopByHopHeaders(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Connection", "close")
|
|
|
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
|
_, _ = io.WriteString(w, "layer-data")
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.String(); got != "layer-data" {
|
|
|
|
|
t.Fatalf("body = %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Connection"); got != "" {
|
|
|
|
|
t.Fatalf("Connection header leaked: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryCachesAnonymousManifestByAccept(t *testing.T) {
|
|
|
|
|
var hits int32
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
count := atomic.AddInt32(&hits, 1)
|
|
|
|
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
|
|
|
|
w.Header().Set("Content-Type", r.Header.Get("Accept"))
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Header.Set("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), `"hit":1`) {
|
|
|
|
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
|
|
|
t.Fatalf("hits = %d, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Header.Set("Accept", "application/vnd.oci.image.index.v1+json")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("second accept status = %d; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), `"hit":2`) {
|
|
|
|
|
t.Fatalf("second accept body = %q", w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryDoesNotCacheAuthenticatedManifest(t *testing.T) {
|
|
|
|
|
var hits int32
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
count := atomic.AddInt32(&hits, 1)
|
|
|
|
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
|
|
|
|
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
for i := 1; i <= 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer token")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), fmt.Sprintf(`"hit":%d`, i)) {
|
|
|
|
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 2 {
|
|
|
|
|
t.Fatalf("hits = %d, want 2", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryCachesKnownAnonymousBearerManifest(t *testing.T) {
|
|
|
|
|
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
|
|
|
|
anonymousTokens.RememberFromResponse([]byte(`{"token":"anonymous-token","expires_in":3600}`), time.Hour)
|
|
|
|
|
|
|
|
|
|
var hits int32
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
count := atomic.AddInt32(&hits, 1)
|
|
|
|
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
|
|
|
|
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer anonymous-token")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), `"hit":1`) {
|
|
|
|
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
|
|
|
t.Fatalf("hits = %d, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryDoesNotCacheUnknownBearerManifest(t *testing.T) {
|
|
|
|
|
anonymousTokens = &anonymousTokenStore{entries: make(map[string]time.Time)}
|
|
|
|
|
|
|
|
|
|
var hits int32
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
count := atomic.AddInt32(&hits, 1)
|
|
|
|
|
body := fmt.Sprintf(`{"schemaVersion":2,"hit":%d}`, count)
|
|
|
|
|
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
|
|
|
_, _ = w.Write([]byte(body))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
for i := 1; i <= 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/manifests/latest", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Bearer user-token")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(w.Body.String(), fmt.Sprintf(`"hit":%d`, i)) {
|
|
|
|
|
t.Fatalf("request %d body = %q", i, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 2 {
|
|
|
|
|
t.Fatalf("hits = %d, want 2", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryUsesNsQueryForContainerd(t *testing.T) {
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if r.URL.Path != "/v2/team/app/manifests/latest" {
|
|
|
|
|
t.Fatalf("upstream path = %q", r.URL.Path)
|
|
|
|
|
}
|
|
|
|
|
if got := r.URL.Query().Get("ns"); got != "test.local" {
|
|
|
|
|
t.Fatalf("ns query = %q", got)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/team/app/manifests/latest?ns=test.local", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerAuthCachesOnlyAnonymousTokenRequests(t *testing.T) {
|
|
|
|
|
var hits int32
|
|
|
|
|
authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
count := atomic.AddInt32(&hits, 1)
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
_, _ = fmt.Fprintf(w, `{"token":"token-%d","expires_in":3600}`, count)
|
|
|
|
|
}))
|
|
|
|
|
defer authServer.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "https://test.local"
|
|
|
|
|
authHost = "`+authServer.URL+`"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
utils.GlobalCache = &utils.UniversalCache{}
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/token/*path", ProxyDockerAuthGin)
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("anonymous request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if got := w.Body.String(); !strings.Contains(got, `"token":"token-1"`) {
|
|
|
|
|
t.Fatalf("anonymous request %d body = %q", i, got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 1 {
|
|
|
|
|
t.Fatalf("anonymous token hits = %d, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/test.local?scope=repository:team/app:pull", nil)
|
|
|
|
|
req.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Fatalf("authenticated request %d status = %d; body=%s", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != 3 {
|
|
|
|
|
t.Fatalf("authenticated token hits total = %d, want 3", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerAuthRejectsUnknownRegistry(t *testing.T) {
|
|
|
|
|
initDockerProxyTest(t, "")
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/token/*path", ProxyDockerAuthGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/token/missing.local?scope=repository:team/app:pull", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Fatalf("status = %d, want 400; body=%s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryConcurrentRequests(t *testing.T) {
|
|
|
|
|
const requests = 64
|
|
|
|
|
var hits int32
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
atomic.AddInt32(&hits, 1)
|
|
|
|
|
if got := r.Header.Get("Authorization"); got == "" {
|
|
|
|
|
t.Fatal("missing Authorization")
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
|
_, _ = w.Write([]byte("ok"))
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
errs := make(chan string, requests)
|
|
|
|
|
for i := 0; i < requests; i++ {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func(i int) {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:abc", nil)
|
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer token-%d", i))
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.Code != http.StatusOK || w.Body.String() != "ok" {
|
|
|
|
|
errs <- fmt.Sprintf("request %d status=%d body=%q", i, w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}(i)
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
|
|
|
|
close(errs)
|
|
|
|
|
|
|
|
|
|
for err := range errs {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if got := atomic.LoadInt32(&hits); got != requests {
|
|
|
|
|
t.Fatalf("hits = %d, want %d", got, requests)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProxyDockerRegistryLargeBlobStreamsWithoutRecorderBuffer(t *testing.T) {
|
|
|
|
|
const blobSize = 8 << 20
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", blobSize))
|
|
|
|
|
_, _ = io.CopyN(w, zeroReader{}, blobSize)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(t, `
|
|
|
|
|
[registries."test.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.test.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/test.local/team/app/blobs/sha256:large", nil)
|
|
|
|
|
w := newDiscardResponseWriter()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
if w.status != http.StatusOK {
|
|
|
|
|
t.Fatalf("status = %d, want 200", w.status)
|
|
|
|
|
}
|
|
|
|
|
if w.bytes != blobSize {
|
|
|
|
|
t.Fatalf("streamed bytes = %d, want %d", w.bytes, blobSize)
|
|
|
|
|
}
|
|
|
|
|
if got := w.Header().Get("Content-Length"); got != fmt.Sprintf("%d", blobSize) {
|
|
|
|
|
t.Fatalf("Content-Length = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BenchmarkProxyDockerRegistryBlobStreaming(b *testing.B) {
|
|
|
|
|
const blobSize = 1 << 20
|
|
|
|
|
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", blobSize))
|
|
|
|
|
_, _ = io.CopyN(w, zeroReader{}, blobSize)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
initDockerProxyTest(b, `
|
|
|
|
|
[registries."bench.local"]
|
|
|
|
|
upstream = "`+upstream.URL+`"
|
|
|
|
|
authHost = "https://auth.bench.local/token"
|
|
|
|
|
authType = "anonymous"
|
|
|
|
|
enabled = true
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Any("/v2/*path", ProxyDockerRegistryGin)
|
|
|
|
|
|
|
|
|
|
b.ReportAllocs()
|
|
|
|
|
b.SetBytes(blobSize)
|
|
|
|
|
b.ResetTimer()
|
|
|
|
|
|
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v2/bench.local/team/app/blobs/sha256:bench", nil)
|
|
|
|
|
w := newDiscardResponseWriter()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
if w.status != http.StatusOK || w.bytes != blobSize {
|
|
|
|
|
b.Fatalf("status=%d bytes=%d", w.status, w.bytes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|