Skip to content

Commit d1502af

Browse files
author
Mike Goelzer
committed
Pass upstream client's user agent through to registry on image pulls
Changes how the Engine interacts with Registry servers on image pull. Previously, Engine sent a User-Agent string to the Registry server that included only the Engine's version information. This commit appends to that string the fields from the User-Agent sent by the client (e.g., Compose) of the Engine. This allows Registry server operators to understand what tools are actually generating pulls on their registries. Signed-off-by: Mike Goelzer <[email protected]>
1 parent 581fc53 commit d1502af

11 files changed

Lines changed: 163 additions & 16 deletions

File tree

api/client/trust.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, aut
152152
}
153153

154154
// Skip configuration headers since request is not going to Docker daemon
155-
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), http.Header{})
155+
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(""), http.Header{})
156156
authTransport := transport.NewTransport(base, modifiers...)
157157
pingClient := &http.Client{
158158
Transport: authTransport,

api/server/httputils/httputils.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import (
1616
// APIVersionKey is the client's requested API version.
1717
const APIVersionKey = "api-version"
1818

19+
// UAStringKey is used as key type for user-agent string in net/context struct
20+
const UAStringKey = "upstream-user-agent"
21+
1922
// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints.
2023
// Any function that has the appropriate signature can be registered as a API endpoint (e.g. getVersion).
2124
type APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error

api/server/middleware/user_agent.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ func NewUserAgentMiddleware(versionCheck string) Middleware {
1616

1717
return func(handler httputils.APIFunc) httputils.APIFunc {
1818
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
19+
ctx = context.WithValue(ctx, httputils.UAStringKey, r.Header.Get("User-Agent"))
20+
1921
if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
2022
userAgent := strings.Split(r.Header.Get("User-Agent"), "/")
2123

api/server/router/image/backend.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/docker/engine-api/types"
88
"github.com/docker/engine-api/types/container"
99
"github.com/docker/engine-api/types/registry"
10+
"golang.org/x/net/context"
1011
)
1112

1213
// Backend is all the methods that need to be implemented
@@ -37,7 +38,7 @@ type importExportBackend interface {
3738
}
3839

3940
type registryBackend interface {
40-
PullImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
41+
PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
4142
PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
4243
SearchRegistryForImages(term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
4344
}

api/server/router/image/image_routes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
129129
}
130130
}
131131

132-
err = s.backend.PullImage(ref, metaHeaders, authConfig, output)
132+
err = s.backend.PullImage(ctx, ref, metaHeaders, authConfig, output)
133133
}
134134
}
135135
// Check the error from pulling an image to make sure the request

daemon/daemon.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,14 +1007,14 @@ func isBrokenPipe(e error) bool {
10071007

10081008
// PullImage initiates a pull operation. image is the repository name to pull, and
10091009
// tag may be either empty, or indicate a specific tag to pull.
1010-
func (daemon *Daemon) PullImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
1010+
func (daemon *Daemon) PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
10111011
// Include a buffer so that slow client connections don't affect
10121012
// transfer performance.
10131013
progressChan := make(chan progress.Progress, 100)
10141014

10151015
writesDone := make(chan struct{})
10161016

1017-
ctx, cancelFunc := context.WithCancel(context.Background())
1017+
ctx, cancelFunc := context.WithCancel(ctx)
10181018

10191019
go func() {
10201020
writeDistributionProgress(cancelFunc, outStream, progressChan)
@@ -1062,7 +1062,7 @@ func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.Auth
10621062
pullRegistryAuth = &resolvedConfig
10631063
}
10641064

1065-
if err := daemon.PullImage(ref, nil, pullRegistryAuth, output); err != nil {
1065+
if err := daemon.PullImage(context.Background(), ref, nil, pullRegistryAuth, output); err != nil {
10661066
return nil, err
10671067
}
10681068
return daemon.GetImage(name)
@@ -1519,15 +1519,15 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
15191519

15201520
// AuthenticateToRegistry checks the validity of credentials in authConfig
15211521
func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) {
1522-
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
1522+
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(""))
15231523
}
15241524

15251525
// SearchRegistryForImages queries the registry for images matching
15261526
// term. authConfig is used to login.
15271527
func (daemon *Daemon) SearchRegistryForImages(term string,
15281528
authConfig *types.AuthConfig,
15291529
headers map[string][]string) (*registrytypes.SearchResults, error) {
1530-
return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(), headers)
1530+
return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(""), headers)
15311531
}
15321532

15331533
// IsShuttingDown tells whether the daemon is shutting down or not

distribution/pull_v1.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
4949
tr := transport.NewTransport(
5050
// TODO(tiborvass): was ReceiveTimeout
5151
registry.NewTransport(tlsConfig),
52-
registry.DockerHeaders(dockerversion.DockerUserAgent(), p.config.MetaHeaders)...,
52+
registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
5353
)
5454
client := registry.HTTPClient(tr)
55-
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), p.config.MetaHeaders)
55+
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
5656
if err != nil {
5757
logrus.Debugf("Could not get v1 endpoint: %v", err)
5858
return fallbackError{err: err}

distribution/push_v1.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ func (p *v1Pusher) Push(ctx context.Context) error {
3838
tr := transport.NewTransport(
3939
// TODO(tiborvass): was NoTimeout
4040
registry.NewTransport(tlsConfig),
41-
registry.DockerHeaders(dockerversion.DockerUserAgent(), p.config.MetaHeaders)...,
41+
registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
4242
)
4343
client := registry.HTTPClient(tr)
44-
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(), p.config.MetaHeaders)
44+
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
4545
if err != nil {
4646
logrus.Debugf("Could not get v1 endpoint: %v", err)
4747
return fallbackError{err: err}

distribution/registry.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
3737
// providing timeout settings and authentication support, and also verifies the
3838
// remote API version.
3939
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+
4042
repoName := repoInfo.FullName()
4143
// If endpoint does not support CanonicalName, use the RemoteName instead
4244
if endpoint.TrimHostname {
@@ -57,7 +59,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
5759
DisableKeepAlives: true,
5860
}
5961

60-
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders)
62+
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(upstreamUA), metaHeaders)
6163
authTransport := transport.NewTransport(base, modifiers...)
6264

6365
challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)

dockerversion/useragent.go

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package dockerversion
22

33
import (
4+
"fmt"
45
"runtime"
56

7+
"github.com/docker/docker/api/server/httputils"
68
"github.com/docker/docker/pkg/parsers/kernel"
79
"github.com/docker/docker/pkg/useragent"
10+
"golang.org/x/net/context"
811
)
912

1013
// DockerUserAgent is the User-Agent the Docker client uses to identify itself.
11-
// It is populated from version information of different components.
12-
func DockerUserAgent() string {
14+
// In accordance with RFC 7231 (5.5.3) is of the form:
15+
// [docker client's UA] UpstreamClient([upstream client's UA])
16+
func DockerUserAgent(upstreamUA string) string {
1317
httpVersion := make([]useragent.VersionInfo, 0, 6)
1418
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
1519
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
@@ -20,5 +24,50 @@ func DockerUserAgent() string {
2024
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
2125
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
2226

23-
return useragent.AppendVersions("", httpVersion...)
27+
dockerUA := useragent.AppendVersions("", httpVersion...)
28+
if len(upstreamUA) > 0 {
29+
ret := insertUpstreamUserAgent(upstreamUA, dockerUA)
30+
return ret
31+
}
32+
return dockerUA
33+
}
34+
35+
// GetUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists
36+
func GetUserAgentFromContext(ctx context.Context) string {
37+
var upstreamUA string
38+
if ctx != nil {
39+
var ki interface{} = ctx.Value(httputils.UAStringKey)
40+
if ki != nil {
41+
upstreamUA = ctx.Value(httputils.UAStringKey).(string)
42+
}
43+
}
44+
return upstreamUA
45+
}
46+
47+
// escapeStr returns s with every rune in charsToEscape escaped by a backslash
48+
func escapeStr(s string, charsToEscape string) string {
49+
var ret string
50+
for _, currRune := range s {
51+
appended := false
52+
for _, escapeableRune := range charsToEscape {
53+
if currRune == escapeableRune {
54+
ret += "\\" + string(currRune)
55+
appended = true
56+
break
57+
}
58+
}
59+
if !appended {
60+
ret += string(currRune)
61+
}
62+
}
63+
return ret
64+
}
65+
66+
// insertUpstreamUserAgent adds the upstream client useragent to create a user-agent
67+
// string of the form:
68+
// $dockerUA UpstreamClient($upstreamUA)
69+
func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string {
70+
charsToEscape := "();\\" //["\\", ";", "(", ")"]string
71+
upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape)
72+
return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped)
2473
}

0 commit comments

Comments
 (0)