Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Support basic auth and HTTP for image registries
Browse files Browse the repository at this point in the history
The image registry code assumed
 - all image registries use HTTPS
 - all image registries use token authentication

..but if you run your own registry in your cluster, neither of these
things is likely to be true.

To support basic authentication, all we need to do is add a handler in
when constructing the registry client; it will get used if the
authentication challenge indicates so.

Supporting HTTP is (oddly) a bit trickier, since there's no indication
from an image name (which is all we have) whether the registry uses
HTTP or HTTPS. Luckily, registries will tend to redirect any HTTP
requests to HTTPS, so if we're not sure we can simply try HTTP first
and follow any redirect.
  • Loading branch information
squaremo committed Jan 25, 2018
1 parent 9ce750a commit be35a3a
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 32 deletions.
13 changes: 7 additions & 6 deletions registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,25 @@ type ClientFactory interface {
type Remote struct {
transport http.RoundTripper
repo image.CanonicalName
base string
}

// Adapt to docker distribution `reference.Named`.
type named struct {
image.CanonicalName
}

// Name returns the name of the repository. These values are used to
// build API URLs, and (it turns out) are _not_ expected to include a
// domain (e.g., quay.io). Hence, the implementation here just returns
// the path.
// Name returns the name of the repository. These values are used by
// the docker distribution client package to build API URLs, and (it
// turns out) are _not_ expected to include a domain (e.g.,
// quay.io). Hence, the implementation here just returns the path.
func (n named) Name() string {
return n.Image
}

// Return the tags for this repository.
func (a *Remote) Tags(ctx context.Context) ([]string, error) {
repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
if err != nil {
return nil, err
}
Expand All @@ -64,7 +65,7 @@ func (a *Remote) Tags(ctx context.Context) ([]string, error) {
// Manifest fetches the metadata for an image reference; currently
// assumed to be in the same repo as that provided to `NewRemote(...)`
func (a *Remote) Manifest(ctx context.Context, ref string) (image.Info, error) {
repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
if err != nil {
return image.Info{}, err
}
Expand Down
79 changes: 53 additions & 26 deletions registry/client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import (
)

type RemoteClientFactory struct {
Logger log.Logger
Limiters *middleware.RateLimiters
Trace bool
Logger log.Logger
Limiters *middleware.RateLimiters
Trace bool

mu sync.Mutex
challengeManager challenge.Manager
mx sync.Mutex
}

type logging struct {
Expand All @@ -43,47 +44,73 @@ func (f *RemoteClientFactory) ClientFor(repo image.CanonicalName, creds Credenti
tx = &logging{f.Logger, tx}
}

f.mx.Lock()
f.mu.Lock()
if f.challengeManager == nil {
f.challengeManager = challenge.NewSimpleManager()
}
f.mx.Unlock()
manager := f.challengeManager
f.mu.Unlock()

pingURL := url.URL{
registryURL := url.URL{
Scheme: "https",
Host: repo.Domain,
Path: "/v2/",
}

// Before we know how to authorise, need to establish which
// authorisation challenges the host will send.
if cs, err := manager.GetChallenges(pingURL); err == nil {
if len(cs) == 0 {
req, err := http.NewRequest("GET", pingURL.String(), nil)
if err != nil {
return nil, err
}
res, err := (&http.Client{
Transport: tx,
}).Do(req)
if err != nil {
return nil, err
}
if err = manager.AddResponse(res); err != nil {
return nil, err
}
cs, err := manager.GetChallenges(registryURL)
if err != nil {
return nil, err
}
if len(cs) == 0 {
// maybe it's HTTP rather than HTTPS?
registryURL.Scheme = "http"
cs, err = manager.GetChallenges(registryURL)
if err != nil {
return nil, err
}
}

if len(cs) == 0 {
// Still no joy; try pinging the registry endpoint to get a
// challenge. This time we start with HTTP, since that will
// (usually) get redirected to HTTPS if the registry accepts
// only the latter. `http.Client` will follow redirects by
// default.
pingURL := url.URL{
Scheme: "http",
Host: repo.Domain,
Path: "/v2/",
}
req, err := http.NewRequest("GET", pingURL.String(), nil)
if err != nil {
return nil, err
}
res, err := (&http.Client{
Transport: tx,
}).Do(req)
if err != nil {
return nil, err
}
if err = manager.AddResponse(res); err != nil {
return nil, err
}
registryURL = *res.Request.URL // <- the URL after any redirection
}

cred := creds.credsFor(repo.Domain)
if f.Trace {
f.Logger.Log("repo", repo.String(), "auth", cred.String())
f.Logger.Log("repo", repo.String(), "auth", cred.String(), "api", registryURL.String())
}

handler := auth.NewTokenHandler(tx, &store{cred}, repo.Image, "pull")
tx = transport.NewTransport(tx, auth.NewAuthorizer(manager, handler))
tokenHandler := auth.NewTokenHandler(tx, &store{cred}, repo.Image, "pull")
basicauthHandler := auth.NewBasicHandler(&store{cred})
tx = transport.NewTransport(tx, auth.NewAuthorizer(manager, tokenHandler, basicauthHandler))

client := &Remote{transport: tx, repo: repo}
// For the registry API base we want only the scheme and host.
registryURL.Path = ""
client := &Remote{transport: tx, repo: repo, base: registryURL.String()}
return NewInstrumentedClient(client), nil
}

Expand Down

0 comments on commit be35a3a

Please sign in to comment.