Browse Source

Support basic auth and HTTP for image registries

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. Registries will tend to redirect any HTTP
requests to HTTPS, so we _could_ try HTTP first and follow any
redirection. However, if a registry supported both HTTP and HTTPS, and
didn't redirect, we'd end up sending credentials over an insecure
connection unnecessarily.

Instead of that, make it possible to tell the daemon which registries
to use HTTP for, with the (multiply-valued) argument
`--registry-insecure-host`.
Michael Bridgen 2 years ago
parent
commit
4d3ef253a0
3 changed files with 61 additions and 37 deletions
  1. 5
    3
      cmd/fluxd/main.go
  2. 7
    6
      registry/client.go
  3. 49
    28
      registry/client_factory.go

+ 5
- 3
cmd/fluxd/main.go View File

@@ -94,6 +94,7 @@ func main() {
94 94
 		registryRPS          = fs.Int("registry-rps", 200, "maximum registry requests per second per host")
95 95
 		registryBurst        = fs.Int("registry-burst", defaultRemoteConnections, "maximum number of warmer connections to remote and memcache")
96 96
 		registryTrace        = fs.Bool("registry-trace", false, "output trace of image registry requests to log")
97
+		registryInsecure     = fs.StringSlice("registry-insecure-host", []string{}, "use HTTP for this image registry domain (e.g., registry.cluster.local), instead of HTTPS")
97 98
 
98 99
 		// k8s-secret backed ssh keyring configuration
99 100
 		k8sSecretName            = fs.String("k8s-secret-name", "flux-git-deploy", "Name of the k8s secret used to store the private SSH key")
@@ -257,9 +258,10 @@ func main() {
257 258
 			Burst: *registryBurst,
258 259
 		}
259 260
 		remoteFactory := &registry.RemoteClientFactory{
260
-			Logger:   registryLogger,
261
-			Limiters: registryLimits,
262
-			Trace:    *registryTrace,
261
+			Logger:        registryLogger,
262
+			Limiters:      registryLimits,
263
+			Trace:         *registryTrace,
264
+			InsecureHosts: *registryInsecure,
263 265
 		}
264 266
 
265 267
 		// Warmer

+ 7
- 6
registry/client.go View File

@@ -37,6 +37,7 @@ type ClientFactory interface {
37 37
 type Remote struct {
38 38
 	transport http.RoundTripper
39 39
 	repo      image.CanonicalName
40
+	base      string
40 41
 }
41 42
 
42 43
 // Adapt to docker distribution `reference.Named`.
@@ -44,17 +45,17 @@ type named struct {
44 45
 	image.CanonicalName
45 46
 }
46 47
 
47
-// Name returns the name of the repository. These values are used to
48
-// build API URLs, and (it turns out) are _not_ expected to include a
49
-// domain (e.g., quay.io). Hence, the implementation here just returns
50
-// the path.
48
+// Name returns the name of the repository. These values are used by
49
+// the docker distribution client package to build API URLs, and (it
50
+// turns out) are _not_ expected to include a domain (e.g.,
51
+// quay.io). Hence, the implementation here just returns the path.
51 52
 func (n named) Name() string {
52 53
 	return n.Image
53 54
 }
54 55
 
55 56
 // Return the tags for this repository.
56 57
 func (a *Remote) Tags(ctx context.Context) ([]string, error) {
57
-	repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
58
+	repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
58 59
 	if err != nil {
59 60
 		return nil, err
60 61
 	}
@@ -64,7 +65,7 @@ func (a *Remote) Tags(ctx context.Context) ([]string, error) {
64 65
 // Manifest fetches the metadata for an image reference; currently
65 66
 // assumed to be in the same repo as that provided to `NewRemote(...)`
66 67
 func (a *Remote) Manifest(ctx context.Context, ref string) (image.Info, error) {
67
-	repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
68
+	repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
68 69
 	if err != nil {
69 70
 		return image.Info{}, err
70 71
 	}

+ 49
- 28
registry/client_factory.go View File

@@ -15,11 +15,13 @@ import (
15 15
 )
16 16
 
17 17
 type RemoteClientFactory struct {
18
-	Logger           log.Logger
19
-	Limiters         *middleware.RateLimiters
20
-	Trace            bool
18
+	Logger        log.Logger
19
+	Limiters      *middleware.RateLimiters
20
+	Trace         bool
21
+	InsecureHosts []string
22
+
23
+	mu               sync.Mutex
21 24
 	challengeManager challenge.Manager
22
-	mx               sync.Mutex
23 25
 }
24 26
 
25 27
 type logging struct {
@@ -43,47 +45,66 @@ func (f *RemoteClientFactory) ClientFor(repo image.CanonicalName, creds Credenti
43 45
 		tx = &logging{f.Logger, tx}
44 46
 	}
45 47
 
46
-	f.mx.Lock()
48
+	f.mu.Lock()
47 49
 	if f.challengeManager == nil {
48 50
 		f.challengeManager = challenge.NewSimpleManager()
49 51
 	}
50
-	f.mx.Unlock()
51 52
 	manager := f.challengeManager
53
+	f.mu.Unlock()
54
+
55
+	scheme := "https"
56
+	for _, h := range f.InsecureHosts {
57
+		if repo.Domain == h {
58
+			scheme = "http"
59
+		}
60
+	}
52 61
 
53
-	pingURL := url.URL{
54
-		Scheme: "https",
62
+	registryURL := url.URL{
63
+		Scheme: scheme,
55 64
 		Host:   repo.Domain,
56 65
 		Path:   "/v2/",
57 66
 	}
67
+
58 68
 	// Before we know how to authorise, need to establish which
59
-	// authorisation challenges the host will send.
60
-	if cs, err := manager.GetChallenges(pingURL); err == nil {
61
-		if len(cs) == 0 {
62
-			req, err := http.NewRequest("GET", pingURL.String(), nil)
63
-			if err != nil {
64
-				return nil, err
65
-			}
66
-			res, err := (&http.Client{
67
-				Transport: tx,
68
-			}).Do(req)
69
-			if err != nil {
70
-				return nil, err
71
-			}
72
-			if err = manager.AddResponse(res); err != nil {
73
-				return nil, err
74
-			}
69
+	// authorisation challenges the host will send. See if we've been
70
+	// here before.
71
+	cs, err := manager.GetChallenges(registryURL)
72
+	if err != nil {
73
+		return nil, err
74
+	}
75
+	if len(cs) == 0 {
76
+		// No prior challenge; try pinging the registry endpoint to
77
+		// get a challenge. `http.Client` will follow redirects, so
78
+		// even if we thought it was an insecure (HTTP) host, we may
79
+		// end up requesting HTTPS.
80
+		req, err := http.NewRequest("GET", registryURL.String(), nil)
81
+		if err != nil {
82
+			return nil, err
83
+		}
84
+		res, err := (&http.Client{
85
+			Transport: tx,
86
+		}).Do(req)
87
+		if err != nil {
88
+			return nil, err
89
+		}
90
+		if err = manager.AddResponse(res); err != nil {
91
+			return nil, err
75 92
 		}
93
+		registryURL = *res.Request.URL // <- the URL after any redirection
76 94
 	}
77 95
 
78 96
 	cred := creds.credsFor(repo.Domain)
79 97
 	if f.Trace {
80
-		f.Logger.Log("repo", repo.String(), "auth", cred.String())
98
+		f.Logger.Log("repo", repo.String(), "auth", cred.String(), "api", registryURL.String())
81 99
 	}
82 100
 
83
-	handler := auth.NewTokenHandler(tx, &store{cred}, repo.Image, "pull")
84
-	tx = transport.NewTransport(tx, auth.NewAuthorizer(manager, handler))
101
+	tokenHandler := auth.NewTokenHandler(tx, &store{cred}, repo.Image, "pull")
102
+	basicauthHandler := auth.NewBasicHandler(&store{cred})
103
+	tx = transport.NewTransport(tx, auth.NewAuthorizer(manager, tokenHandler, basicauthHandler))
85 104
 
86
-	client := &Remote{transport: tx, repo: repo}
105
+	// For the API base we want only the scheme and host.
106
+	registryURL.Path = ""
107
+	client := &Remote{transport: tx, repo: repo, base: registryURL.String()}
87 108
 	return NewInstrumentedClient(client), nil
88 109
 }
89 110
 

Loading…
Cancel
Save