Browse code

Pass upstream client's user agent through to registry on operations beyond pulls

This adds support for the passthrough on build, push, login, and search.

Revamp the integration test to cover these cases and make it more
robust.

Use backticks instead of quoted strings for backslash-heavy string
contstands.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2016/03/19 06:42:40
Showing 17 changed files
... ...
@@ -140,7 +140,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
140 140
 		if customHeaders == nil {
141 141
 			customHeaders = map[string]string{}
142 142
 		}
143
-		customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
143
+		customHeaders["User-Agent"] = clientUserAgent()
144 144
 
145 145
 		verStr := api.DefaultVersion.String()
146 146
 		if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
... ...
@@ -209,3 +209,7 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
209 209
 		Transport: tr,
210 210
 	}, nil
211 211
 }
212
+
213
+func clientUserAgent() string {
214
+	return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
215
+}
... ...
@@ -23,7 +23,6 @@ import (
23 23
 	"github.com/docker/distribution/registry/client/transport"
24 24
 	"github.com/docker/docker/cliconfig"
25 25
 	"github.com/docker/docker/distribution"
26
-	"github.com/docker/docker/dockerversion"
27 26
 	"github.com/docker/docker/pkg/jsonmessage"
28 27
 	flag "github.com/docker/docker/pkg/mflag"
29 28
 	"github.com/docker/docker/reference"
... ...
@@ -152,7 +151,7 @@ func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, aut
152 152
 	}
153 153
 
154 154
 	// Skip configuration headers since request is not going to Docker daemon
155
-	modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(""), http.Header{})
155
+	modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{})
156 156
 	authTransport := transport.NewTransport(base, modifiers...)
157 157
 	pingClient := &http.Client{
158 158
 		Transport: authTransport,
... ...
@@ -1,9 +1,11 @@
1 1
 package build
2 2
 
3 3
 import (
4
+	"io"
5
+
4 6
 	"github.com/docker/docker/builder"
5 7
 	"github.com/docker/engine-api/types"
6
-	"io"
8
+	"golang.org/x/net/context"
7 9
 )
8 10
 
9 11
 // Backend abstracts an image builder whose only purpose is to build an image referenced by an imageID.
... ...
@@ -14,5 +16,5 @@ type Backend interface {
14 14
 	// by the caller.
15 15
 	//
16 16
 	// TODO: make this return a reference instead of string
17
-	Build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error)
17
+	Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error)
18 18
 }
... ...
@@ -171,7 +171,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
171 171
 		closeNotifier = notifier.CloseNotify()
172 172
 	}
173 173
 
174
-	imgID, err := br.backend.Build(buildOptions,
174
+	imgID, err := br.backend.Build(ctx, buildOptions,
175 175
 		builder.DockerIgnoreContext{ModifiableContext: context},
176 176
 		stdout, stderr, out,
177 177
 		closeNotifier)
... ...
@@ -39,6 +39,6 @@ type importExportBackend interface {
39 39
 
40 40
 type registryBackend interface {
41 41
 	PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
42
-	PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
43
-	SearchRegistryForImages(term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
42
+	PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
43
+	SearchRegistryForImages(ctx context.Context, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
44 44
 }
... ...
@@ -228,7 +228,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
228 228
 
229 229
 	w.Header().Set("Content-Type", "application/json")
230 230
 
231
-	if err := s.backend.PushImage(ref, metaHeaders, authConfig, output); err != nil {
231
+	if err := s.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil {
232 232
 		if !output.Flushed() {
233 233
 			return err
234 234
 		}
... ...
@@ -373,7 +373,7 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
373 373
 			headers[k] = v
374 374
 		}
375 375
 	}
376
-	query, err := s.backend.SearchRegistryForImages(r.Form.Get("term"), config, headers)
376
+	query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("term"), config, headers)
377 377
 	if err != nil {
378 378
 		return err
379 379
 	}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"github.com/docker/engine-api/types"
5 5
 	"github.com/docker/engine-api/types/events"
6 6
 	"github.com/docker/engine-api/types/filters"
7
+	"golang.org/x/net/context"
7 8
 )
8 9
 
9 10
 // Backend is the methods that need to be implemented to provide
... ...
@@ -13,5 +14,5 @@ type Backend interface {
13 13
 	SystemVersion() types.Version
14 14
 	SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
15 15
 	UnsubscribeFromEvents(chan interface{})
16
-	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
16
+	AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error)
17 17
 }
... ...
@@ -115,7 +115,7 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
115 115
 	if err != nil {
116 116
 		return err
117 117
 	}
118
-	status, token, err := s.backend.AuthenticateToRegistry(config)
118
+	status, token, err := s.backend.AuthenticateToRegistry(ctx, config)
119 119
 	if err != nil {
120 120
 		return err
121 121
 	}
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/docker/docker/reference"
13 13
 	"github.com/docker/engine-api/types"
14 14
 	"github.com/docker/engine-api/types/container"
15
+	"golang.org/x/net/context"
15 16
 )
16 17
 
17 18
 const (
... ...
@@ -109,7 +110,7 @@ type Backend interface {
109 109
 	// Tag an image with newTag
110 110
 	TagImage(newTag reference.Named, imageName string) error
111 111
 	// Pull tells Docker to pull image referenced by `name`.
112
-	PullOnBuild(name string, authConfigs map[string]types.AuthConfig, output io.Writer) (Image, error)
112
+	PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (Image, error)
113 113
 	// ContainerAttach attaches to container.
114 114
 	ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool) error
115 115
 	// ContainerCreate creates a new Docker container and returns potential warnings
... ...
@@ -17,6 +17,7 @@ import (
17 17
 	"github.com/docker/docker/reference"
18 18
 	"github.com/docker/engine-api/types"
19 19
 	"github.com/docker/engine-api/types/container"
20
+	"golang.org/x/net/context"
20 21
 )
21 22
 
22 23
 var validCommitCommands = map[string]bool{
... ...
@@ -52,8 +53,9 @@ type Builder struct {
52 52
 	Stderr io.Writer
53 53
 	Output io.Writer
54 54
 
55
-	docker  builder.Backend
56
-	context builder.Context
55
+	docker    builder.Backend
56
+	context   builder.Context
57
+	clientCtx context.Context
57 58
 
58 59
 	dockerfile       *parser.Node
59 60
 	runConfig        *container.Config // runconfig for cmd, run, entrypoint etc.
... ...
@@ -86,7 +88,7 @@ func NewBuildManager(b builder.Backend) (bm *BuildManager) {
86 86
 // NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
87 87
 // If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
88 88
 // will be read from the Context passed to Build().
89
-func NewBuilder(config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
89
+func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
90 90
 	if config == nil {
91 91
 		config = new(types.ImageBuildOptions)
92 92
 	}
... ...
@@ -94,6 +96,7 @@ func NewBuilder(config *types.ImageBuildOptions, backend builder.Backend, contex
94 94
 		config.BuildArgs = make(map[string]string)
95 95
 	}
96 96
 	b = &Builder{
97
+		clientCtx:        clientCtx,
97 98
 		options:          config,
98 99
 		Stdout:           os.Stdout,
99 100
 		Stderr:           os.Stderr,
... ...
@@ -158,8 +161,8 @@ func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
158 158
 }
159 159
 
160 160
 // Build creates a NewBuilder, which builds the image.
161
-func (bm *BuildManager) Build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) {
162
-	b, err := NewBuilder(config, bm.backend, context, nil)
161
+func (bm *BuildManager) Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) {
162
+	b, err := NewBuilder(clientCtx, config, bm.backend, context, nil)
163 163
 	if err != nil {
164 164
 		return "", err
165 165
 	}
... ...
@@ -291,7 +294,7 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
291 291
 		}
292 292
 	}
293 293
 
294
-	b, err := NewBuilder(nil, nil, nil, nil)
294
+	b, err := NewBuilder(context.Background(), nil, nil, nil, nil)
295 295
 	if err != nil {
296 296
 		return nil, err
297 297
 	}
... ...
@@ -206,7 +206,7 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
206 206
 			// TODO: shouldn't we error out if error is different from "not found" ?
207 207
 		}
208 208
 		if image == nil {
209
-			image, err = b.docker.PullOnBuild(name, b.options.AuthConfigs, b.Output)
209
+			image, err = b.docker.PullOnBuild(b.clientCtx, name, b.options.AuthConfigs, b.Output)
210 210
 			if err != nil {
211 211
 				return err
212 212
 			}
... ...
@@ -1030,7 +1030,7 @@ func (daemon *Daemon) PullImage(ctx context.Context, ref reference.Named, metaHe
1030 1030
 }
1031 1031
 
1032 1032
 // PullOnBuild tells Docker to pull image referenced by `name`.
1033
-func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.AuthConfig, output io.Writer) (builder.Image, error) {
1033
+func (daemon *Daemon) PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (builder.Image, error) {
1034 1034
 	ref, err := reference.ParseNamed(name)
1035 1035
 	if err != nil {
1036 1036
 		return nil, err
... ...
@@ -1052,7 +1052,7 @@ func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.Auth
1052 1052
 		pullRegistryAuth = &resolvedConfig
1053 1053
 	}
1054 1054
 
1055
-	if err := daemon.PullImage(context.Background(), ref, nil, pullRegistryAuth, output); err != nil {
1055
+	if err := daemon.PullImage(ctx, ref, nil, pullRegistryAuth, output); err != nil {
1056 1056
 		return nil, err
1057 1057
 	}
1058 1058
 	return daemon.GetImage(name)
... ...
@@ -1069,14 +1069,14 @@ func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error {
1069 1069
 }
1070 1070
 
1071 1071
 // PushImage initiates a push operation on the repository named localName.
1072
-func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
1072
+func (daemon *Daemon) PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
1073 1073
 	// Include a buffer so that slow client connections don't affect
1074 1074
 	// transfer performance.
1075 1075
 	progressChan := make(chan progress.Progress, 100)
1076 1076
 
1077 1077
 	writesDone := make(chan struct{})
1078 1078
 
1079
-	ctx, cancelFunc := context.WithCancel(context.Background())
1079
+	ctx, cancelFunc := context.WithCancel(ctx)
1080 1080
 
1081 1081
 	go func() {
1082 1082
 		writeDistributionProgress(cancelFunc, outStream, progressChan)
... ...
@@ -1502,16 +1502,16 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
1502 1502
 }
1503 1503
 
1504 1504
 // AuthenticateToRegistry checks the validity of credentials in authConfig
1505
-func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) {
1506
-	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(""))
1505
+func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
1506
+	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx))
1507 1507
 }
1508 1508
 
1509 1509
 // SearchRegistryForImages queries the registry for images matching
1510 1510
 // term. authConfig is used to login.
1511
-func (daemon *Daemon) SearchRegistryForImages(term string,
1511
+func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, term string,
1512 1512
 	authConfig *types.AuthConfig,
1513 1513
 	headers map[string][]string) (*registrytypes.SearchResults, error) {
1514
-	return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(""), headers)
1514
+	return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
1515 1515
 }
1516 1516
 
1517 1517
 // IsShuttingDown tells whether the daemon is shutting down or not
... ...
@@ -49,10 +49,10 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
49 49
 	tr := transport.NewTransport(
50 50
 		// TODO(tiborvass): was ReceiveTimeout
51 51
 		registry.NewTransport(tlsConfig),
52
-		registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
52
+		registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)...,
53 53
 	)
54 54
 	client := registry.HTTPClient(tr)
55
-	v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
55
+	v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)
56 56
 	if err != nil {
57 57
 		logrus.Debugf("Could not get v1 endpoint: %v", err)
58 58
 		return fallbackError{err: err}
... ...
@@ -38,10 +38,10 @@ func (p *v1Pusher) Push(ctx context.Context) error {
38 38
 	tr := transport.NewTransport(
39 39
 		// TODO(tiborvass): was NoTimeout
40 40
 		registry.NewTransport(tlsConfig),
41
-		registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
41
+		registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)...,
42 42
 	)
43 43
 	client := registry.HTTPClient(tr)
44
-	v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
44
+	v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)
45 45
 	if err != nil {
46 46
 		logrus.Debugf("Could not get v1 endpoint: %v", err)
47 47
 		return fallbackError{err: err}
... ...
@@ -37,8 +37,6 @@ func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
37 37
 // providing timeout settings and authentication support, and also verifies the
38 38
 // remote API version.
39 39
 func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (repo distribution.Repository, foundVersion bool, err error) {
40
-	upstreamUA := dockerversion.GetUserAgentFromContext(ctx)
41
-
42 40
 	repoName := repoInfo.FullName()
43 41
 	// If endpoint does not support CanonicalName, use the RemoteName instead
44 42
 	if endpoint.TrimHostname {
... ...
@@ -59,7 +57,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
59 59
 		DisableKeepAlives: true,
60 60
 	}
61 61
 
62
-	modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(upstreamUA), metaHeaders)
62
+	modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), metaHeaders)
63 63
 	authTransport := transport.NewTransport(base, modifiers...)
64 64
 
65 65
 	challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)
... ...
@@ -13,7 +13,7 @@ import (
13 13
 // DockerUserAgent is the User-Agent the Docker client uses to identify itself.
14 14
 // In accordance with RFC 7231 (5.5.3) is of the form:
15 15
 //    [docker client's UA] UpstreamClient([upstream client's UA])
16
-func DockerUserAgent(upstreamUA string) string {
16
+func DockerUserAgent(ctx context.Context) string {
17 17
 	httpVersion := make([]useragent.VersionInfo, 0, 6)
18 18
 	httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
19 19
 	httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
... ...
@@ -25,6 +25,7 @@ func DockerUserAgent(upstreamUA string) string {
25 25
 	httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
26 26
 
27 27
 	dockerUA := useragent.AppendVersions("", httpVersion...)
28
+	upstreamUA := getUserAgentFromContext(ctx)
28 29
 	if len(upstreamUA) > 0 {
29 30
 		ret := insertUpstreamUserAgent(upstreamUA, dockerUA)
30 31
 		return ret
... ...
@@ -32,8 +33,8 @@ func DockerUserAgent(upstreamUA string) string {
32 32
 	return dockerUA
33 33
 }
34 34
 
35
-// GetUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists
36
-func GetUserAgentFromContext(ctx context.Context) string {
35
+// getUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists
36
+func getUserAgentFromContext(ctx context.Context) string {
37 37
 	var upstreamUA string
38 38
 	if ctx != nil {
39 39
 		var ki interface{} = ctx.Value(httputils.UAStringKey)
... ...
@@ -51,7 +52,7 @@ func escapeStr(s string, charsToEscape string) string {
51 51
 		appended := false
52 52
 		for _, escapeableRune := range charsToEscape {
53 53
 			if currRune == escapeableRune {
54
-				ret += "\\" + string(currRune)
54
+				ret += `\` + string(currRune)
55 55
 				appended = true
56 56
 				break
57 57
 			}
... ...
@@ -67,7 +68,7 @@ func escapeStr(s string, charsToEscape string) string {
67 67
 // string of the form:
68 68
 //   $dockerUA UpstreamClient($upstreamUA)
69 69
 func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string {
70
-	charsToEscape := "();\\" //["\\", ";", "(", ")"]string
70
+	charsToEscape := `();\`
71 71
 	upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape)
72 72
 	return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped)
73 73
 }
... ...
@@ -10,17 +10,17 @@ import (
10 10
 
11 11
 // unescapeBackslashSemicolonParens unescapes \;()
12 12
 func unescapeBackslashSemicolonParens(s string) string {
13
-	re := regexp.MustCompile("\\\\;")
13
+	re := regexp.MustCompile(`\\;`)
14 14
 	ret := re.ReplaceAll([]byte(s), []byte(";"))
15 15
 
16
-	re = regexp.MustCompile("\\\\\\(")
16
+	re = regexp.MustCompile(`\\\(`)
17 17
 	ret = re.ReplaceAll([]byte(ret), []byte("("))
18 18
 
19
-	re = regexp.MustCompile("\\\\\\)")
19
+	re = regexp.MustCompile(`\\\)`)
20 20
 	ret = re.ReplaceAll([]byte(ret), []byte(")"))
21 21
 
22
-	re = regexp.MustCompile("\\\\\\\\")
23
-	ret = re.ReplaceAll([]byte(ret), []byte("\\"))
22
+	re = regexp.MustCompile(`\\\\`)
23
+	ret = re.ReplaceAll([]byte(ret), []byte(`\`))
24 24
 
25 25
 	return string(ret)
26 26
 }
... ...
@@ -46,14 +46,7 @@ func regexpCheckUA(c *check.C, ua string) {
46 46
 	c.Assert(bMatchUpstreamUA, check.Equals, true, check.Commentf("(Upstream) Docker Client User-Agent malformed"))
47 47
 }
48 48
 
49
-// TestUserAgentPassThroughOnPull verifies that when an image is pulled from
50
-// a registry, the registry should see a User-Agent string of the form
51
-//   [docker engine UA] UptreamClientSTREAM-CLIENT([client UA])
52
-func (s *DockerRegistrySuite) TestUserAgentPassThroughOnPull(c *check.C) {
53
-	reg, err := newTestRegistry(c)
54
-	c.Assert(err, check.IsNil)
55
-	expectUpstreamUA := false
56
-
49
+func registerUserAgentHandler(reg *testRegistry, result *string) {
57 50
 	reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) {
58 51
 		w.WriteHeader(404)
59 52
 		var ua string
... ...
@@ -62,29 +55,66 @@ func (s *DockerRegistrySuite) TestUserAgentPassThroughOnPull(c *check.C) {
62 62
 				ua = v[0]
63 63
 			}
64 64
 		}
65
-		c.Assert(ua, check.Not(check.Equals), "", check.Commentf("No User-Agent found in request"))
66
-		if r.URL.Path == "/v2/busybox/manifests/latest" {
67
-			if expectUpstreamUA {
68
-				regexpCheckUA(c, ua)
69
-			}
70
-		}
65
+		*result = ua
71 66
 	})
67
+}
68
+
69
+// TestUserAgentPassThroughOnPull verifies that when an image is pulled from
70
+// a registry, the registry should see a User-Agent string of the form
71
+//   [docker engine UA] UptreamClientSTREAM-CLIENT([client UA])
72
+func (s *DockerRegistrySuite) TestUserAgentPassThrough(c *check.C) {
73
+	var (
74
+		buildUA string
75
+		pullUA  string
76
+		pushUA  string
77
+		loginUA string
78
+	)
79
+
80
+	buildReg, err := newTestRegistry(c)
81
+	c.Assert(err, check.IsNil)
82
+	registerUserAgentHandler(buildReg, &buildUA)
83
+	buildRepoName := fmt.Sprintf("%s/busybox", buildReg.hostport)
72 84
 
73
-	repoName := fmt.Sprintf("%s/busybox", reg.hostport)
74
-	err = s.d.Start("--insecure-registry", reg.hostport, "--disable-legacy-registry=true")
85
+	pullReg, err := newTestRegistry(c)
75 86
 	c.Assert(err, check.IsNil)
87
+	registerUserAgentHandler(pullReg, &pullUA)
88
+	pullRepoName := fmt.Sprintf("%s/busybox", pullReg.hostport)
76 89
 
77
-	dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport))
78
-	c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
79
-	defer cleanup()
90
+	pushReg, err := newTestRegistry(c)
91
+	c.Assert(err, check.IsNil)
92
+	registerUserAgentHandler(pushReg, &pushUA)
93
+	pushRepoName := fmt.Sprintf("%s/busybox", pushReg.hostport)
80 94
 
95
+	loginReg, err := newTestRegistry(c)
96
+	c.Assert(err, check.IsNil)
97
+	registerUserAgentHandler(loginReg, &loginUA)
98
+
99
+	err = s.d.Start(
100
+		"--insecure-registry", buildReg.hostport,
101
+		"--insecure-registry", pullReg.hostport,
102
+		"--insecure-registry", pushReg.hostport,
103
+		"--insecure-registry", loginReg.hostport,
104
+		"--disable-legacy-registry=true")
105
+	c.Assert(err, check.IsNil)
106
+
107
+	dockerfileName, cleanup1, err := makefile(fmt.Sprintf("FROM %s", buildRepoName))
108
+	c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
109
+	defer cleanup1()
81 110
 	s.d.Cmd("build", "--file", dockerfileName, ".")
111
+	regexpCheckUA(c, buildUA)
112
+
113
+	s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", loginReg.hostport)
114
+	regexpCheckUA(c, loginUA)
82 115
 
83
-	s.d.Cmd("run", repoName)
84
-	s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport)
85
-	s.d.Cmd("tag", "busybox", repoName)
86
-	s.d.Cmd("push", repoName)
116
+	s.d.Cmd("pull", pullRepoName)
117
+	regexpCheckUA(c, pullUA)
118
+
119
+	dockerfileName, cleanup2, err := makefile(`FROM scratch
120
+	ENV foo bar`)
121
+	c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
122
+	defer cleanup2()
123
+	s.d.Cmd("build", "-t", pushRepoName, "--file", dockerfileName, ".")
87 124
 
88
-	expectUpstreamUA = true
89
-	s.d.Cmd("pull", repoName)
125
+	s.d.Cmd("push", pushRepoName)
126
+	regexpCheckUA(c, pushUA)
90 127
 }