fix(oci): route authorizer token fetches through provided transport

The transport passed to NewResolver was applied only via
docker.WithClient. Pass it via docker.WithAuthClient too so the
authorizer's OAuth token fetches use it instead of http.DefaultClient.

Signed-off-by: Guillaume Lours <[email protected]>
This commit is contained in:
Guillaume Lours
2026-06-25 15:38:37 +02:00
committed by Guillaume Lours
parent a3c1c0dc2e
commit e87f7b79df
2 changed files with 61 additions and 18 deletions
+20 -15
View File
@@ -38,23 +38,28 @@ import (
// NewResolver sets up an OCI Resolver based on docker/cli config to provide
// registry credentials. When transport is non-nil it is used as the HTTP
// transport for all registry calls (e.g. to route through Docker Desktop's
// PAC-aware proxy); nil falls back to containerd's default transport.
// transport for both registry calls and the authorizer's token fetches
// (e.g. to route both through Docker Desktop's PAC-aware proxy); nil falls
// back to containerd's default transport.
func NewResolver(config *configfile.ConfigFile, transport http.RoundTripper, insecureRegistries ...string) remotes.Resolver {
authOpts := []docker.AuthorizerOpt{
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
}
if transport != nil {
authOpts = append(authOpts, docker.WithAuthClient(&http.Client{Transport: transport}))
}
opts := []docker.RegistryOpt{
docker.WithAuthorizer(docker.NewDockerAuthorizer(
docker.WithAuthCreds(func(host string) (string, string, error) {
host = registry.GetAuthConfigKey(host)
auth, err := config.GetAuthConfig(host)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", auth.IdentityToken, nil
}
return auth.Username, auth.Password, nil
}),
)),
docker.WithAuthorizer(docker.NewDockerAuthorizer(authOpts...)),
docker.WithPlainHTTP(func(domain string) (bool, error) {
// Should be used for testing **only**
return slices.Contains(insecureRegistries, domain), nil
+41 -3
View File
@@ -19,6 +19,7 @@ package oci
import (
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
@@ -27,14 +28,20 @@ import (
)
// recordingRoundTripper counts RoundTrip invocations on a delegate so tests
// can verify a supplied transport is actually used by the resolver.
// can verify a supplied transport is actually used by the resolver. It also
// tracks token-endpoint calls separately so tests can assert the authorizer's
// token fetch goes through the same transport.
type recordingRoundTripper struct {
delegate http.RoundTripper
calls atomic.Int32
delegate http.RoundTripper
calls atomic.Int32
authCalls atomic.Int32
}
func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
r.calls.Add(1)
if strings.HasSuffix(req.URL.Path, "/token") {
r.authCalls.Add(1)
}
return r.delegate.RoundTrip(req)
}
@@ -64,6 +71,37 @@ func TestNewResolver_UsesProvidedTransport(t *testing.T) {
"resolver did not invoke the supplied transport — wiring is broken")
}
// TestNewResolver_AuthorizerUsesProvidedTransport guards that the docker
// authorizer's token fetch goes through the supplied transport, not
// http.DefaultClient.
func TestNewResolver_AuthorizerUsesProvidedTransport(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/token") {
_, _ = w.Write([]byte(`{"token":"fake","access_token":"fake","expires_in":300}`))
return
}
if r.Header.Get("Authorization") == "" {
w.Header().Set("Www-Authenticate", `Bearer realm="`+server.URL+`/token",service="test"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
// Authenticated retry — fail fast, we only care the auth dance went
// through the supplied transport.
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(server.Close)
host := server.Listener.Addr().String()
rec := &recordingRoundTripper{delegate: &http.Transport{}}
resolver := NewResolver(&configfile.ConfigFile{}, rec, host)
_, _, _ = resolver.Resolve(t.Context(), host+"/test/image:latest")
assert.Assert(t, rec.authCalls.Load() > 0,
"authorizer token fetch did not go through the supplied transport (bypassed via http.DefaultClient)")
}
func TestNewResolver_NilTransportIsValid(t *testing.T) {
resolver := NewResolver(&configfile.ConfigFile{}, nil)
assert.Assert(t, resolver != nil, "NewResolver must return a non-nil resolver when transport is nil")