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 <mgoelzer@docker.com>
| ... | ... |
@@ -152,7 +152,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(dockerversion.DockerUserAgent(""), http.Header{})
|
|
| 156 | 156 |
authTransport := transport.NewTransport(base, modifiers...) |
| 157 | 157 |
pingClient := &http.Client{
|
| 158 | 158 |
Transport: authTransport, |
| ... | ... |
@@ -16,6 +16,9 @@ import ( |
| 16 | 16 |
// APIVersionKey is the client's requested API version. |
| 17 | 17 |
const APIVersionKey = "api-version" |
| 18 | 18 |
|
| 19 |
+// UAStringKey is used as key type for user-agent string in net/context struct |
|
| 20 |
+const UAStringKey = "upstream-user-agent" |
|
| 21 |
+ |
|
| 19 | 22 |
// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints. |
| 20 | 23 |
// Any function that has the appropriate signature can be registered as a API endpoint (e.g. getVersion). |
| 21 | 24 |
type APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error |
| ... | ... |
@@ -16,6 +16,8 @@ func NewUserAgentMiddleware(versionCheck string) Middleware {
|
| 16 | 16 |
|
| 17 | 17 |
return func(handler httputils.APIFunc) httputils.APIFunc {
|
| 18 | 18 |
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 |
+ |
|
| 19 | 21 |
if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
|
| 20 | 22 |
userAgent := strings.Split(r.Header.Get("User-Agent"), "/")
|
| 21 | 23 |
|
| ... | ... |
@@ -7,6 +7,7 @@ import ( |
| 7 | 7 |
"github.com/docker/engine-api/types" |
| 8 | 8 |
"github.com/docker/engine-api/types/container" |
| 9 | 9 |
"github.com/docker/engine-api/types/registry" |
| 10 |
+ "golang.org/x/net/context" |
|
| 10 | 11 |
) |
| 11 | 12 |
|
| 12 | 13 |
// Backend is all the methods that need to be implemented |
| ... | ... |
@@ -37,7 +38,7 @@ type importExportBackend interface {
|
| 37 | 37 |
} |
| 38 | 38 |
|
| 39 | 39 |
type registryBackend interface {
|
| 40 |
- PullImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error |
|
| 40 |
+ PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error |
|
| 41 | 41 |
PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error |
| 42 | 42 |
SearchRegistryForImages(term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) |
| 43 | 43 |
} |
| ... | ... |
@@ -129,7 +129,7 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite |
| 129 | 129 |
} |
| 130 | 130 |
} |
| 131 | 131 |
|
| 132 |
- err = s.backend.PullImage(ref, metaHeaders, authConfig, output) |
|
| 132 |
+ err = s.backend.PullImage(ctx, ref, metaHeaders, authConfig, output) |
|
| 133 | 133 |
} |
| 134 | 134 |
} |
| 135 | 135 |
// Check the error from pulling an image to make sure the request |
| ... | ... |
@@ -1007,14 +1007,14 @@ func isBrokenPipe(e error) bool {
|
| 1007 | 1007 |
|
| 1008 | 1008 |
// PullImage initiates a pull operation. image is the repository name to pull, and |
| 1009 | 1009 |
// 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 {
|
|
| 1011 | 1011 |
// Include a buffer so that slow client connections don't affect |
| 1012 | 1012 |
// transfer performance. |
| 1013 | 1013 |
progressChan := make(chan progress.Progress, 100) |
| 1014 | 1014 |
|
| 1015 | 1015 |
writesDone := make(chan struct{})
|
| 1016 | 1016 |
|
| 1017 |
- ctx, cancelFunc := context.WithCancel(context.Background()) |
|
| 1017 |
+ ctx, cancelFunc := context.WithCancel(ctx) |
|
| 1018 | 1018 |
|
| 1019 | 1019 |
go func() {
|
| 1020 | 1020 |
writeDistributionProgress(cancelFunc, outStream, progressChan) |
| ... | ... |
@@ -1062,7 +1062,7 @@ func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.Auth |
| 1062 | 1062 |
pullRegistryAuth = &resolvedConfig |
| 1063 | 1063 |
} |
| 1064 | 1064 |
|
| 1065 |
- if err := daemon.PullImage(ref, nil, pullRegistryAuth, output); err != nil {
|
|
| 1065 |
+ if err := daemon.PullImage(context.Background(), ref, nil, pullRegistryAuth, output); err != nil {
|
|
| 1066 | 1066 |
return nil, err |
| 1067 | 1067 |
} |
| 1068 | 1068 |
return daemon.GetImage(name) |
| ... | ... |
@@ -1519,7 +1519,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore, |
| 1519 | 1519 |
|
| 1520 | 1520 |
// AuthenticateToRegistry checks the validity of credentials in authConfig |
| 1521 | 1521 |
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(""))
|
|
| 1523 | 1523 |
} |
| 1524 | 1524 |
|
| 1525 | 1525 |
// SearchRegistryForImages queries the registry for images matching |
| ... | ... |
@@ -1527,7 +1527,7 @@ func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (stri |
| 1527 | 1527 |
func (daemon *Daemon) SearchRegistryForImages(term string, |
| 1528 | 1528 |
authConfig *types.AuthConfig, |
| 1529 | 1529 |
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)
|
|
| 1531 | 1531 |
} |
| 1532 | 1532 |
|
| 1533 | 1533 |
// 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(""), 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(""), 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(""), 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(""), 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,6 +37,8 @@ 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 |
+ |
|
| 40 | 42 |
repoName := repoInfo.FullName() |
| 41 | 43 |
// If endpoint does not support CanonicalName, use the RemoteName instead |
| 42 | 44 |
if endpoint.TrimHostname {
|
| ... | ... |
@@ -57,7 +59,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end |
| 57 | 57 |
DisableKeepAlives: true, |
| 58 | 58 |
} |
| 59 | 59 |
|
| 60 |
- modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), metaHeaders) |
|
| 60 |
+ modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(upstreamUA), metaHeaders) |
|
| 61 | 61 |
authTransport := transport.NewTransport(base, modifiers...) |
| 62 | 62 |
|
| 63 | 63 |
challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport) |
| ... | ... |
@@ -1,15 +1,19 @@ |
| 1 | 1 |
package dockerversion |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "fmt" |
|
| 4 | 5 |
"runtime" |
| 5 | 6 |
|
| 7 |
+ "github.com/docker/docker/api/server/httputils" |
|
| 6 | 8 |
"github.com/docker/docker/pkg/parsers/kernel" |
| 7 | 9 |
"github.com/docker/docker/pkg/useragent" |
| 10 |
+ "golang.org/x/net/context" |
|
| 8 | 11 |
) |
| 9 | 12 |
|
| 10 | 13 |
// 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 {
|
|
| 13 | 17 |
httpVersion := make([]useragent.VersionInfo, 0, 6) |
| 14 | 18 |
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
|
| 15 | 19 |
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
|
| ... | ... |
@@ -20,5 +24,50 @@ func DockerUserAgent() string {
|
| 20 | 20 |
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
|
| 21 | 21 |
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
|
| 22 | 22 |
|
| 23 |
- return useragent.AppendVersions("", httpVersion...)
|
|
| 23 |
+ dockerUA := useragent.AppendVersions("", httpVersion...)
|
|
| 24 |
+ if len(upstreamUA) > 0 {
|
|
| 25 |
+ ret := insertUpstreamUserAgent(upstreamUA, dockerUA) |
|
| 26 |
+ return ret |
|
| 27 |
+ } |
|
| 28 |
+ return dockerUA |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// GetUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists |
|
| 32 |
+func GetUserAgentFromContext(ctx context.Context) string {
|
|
| 33 |
+ var upstreamUA string |
|
| 34 |
+ if ctx != nil {
|
|
| 35 |
+ var ki interface{} = ctx.Value(httputils.UAStringKey)
|
|
| 36 |
+ if ki != nil {
|
|
| 37 |
+ upstreamUA = ctx.Value(httputils.UAStringKey).(string) |
|
| 38 |
+ } |
|
| 39 |
+ } |
|
| 40 |
+ return upstreamUA |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+// escapeStr returns s with every rune in charsToEscape escaped by a backslash |
|
| 44 |
+func escapeStr(s string, charsToEscape string) string {
|
|
| 45 |
+ var ret string |
|
| 46 |
+ for _, currRune := range s {
|
|
| 47 |
+ appended := false |
|
| 48 |
+ for _, escapeableRune := range charsToEscape {
|
|
| 49 |
+ if currRune == escapeableRune {
|
|
| 50 |
+ ret += "\\" + string(currRune) |
|
| 51 |
+ appended = true |
|
| 52 |
+ break |
|
| 53 |
+ } |
|
| 54 |
+ } |
|
| 55 |
+ if !appended {
|
|
| 56 |
+ ret += string(currRune) |
|
| 57 |
+ } |
|
| 58 |
+ } |
|
| 59 |
+ return ret |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// insertUpstreamUserAgent adds the upstream client useragent to create a user-agent |
|
| 63 |
+// string of the form: |
|
| 64 |
+// $dockerUA UpstreamClient($upstreamUA) |
|
| 65 |
+func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string {
|
|
| 66 |
+ charsToEscape := "();\\" //["\\", ";", "(", ")"]string
|
|
| 67 |
+ upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape) |
|
| 68 |
+ return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped)
|
|
| 24 | 69 |
} |
| 25 | 70 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,90 @@ |
| 0 |
+package main |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "regexp" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/go-check/check" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// unescapeBackslashSemicolonParens unescapes \;() |
|
| 11 |
+func unescapeBackslashSemicolonParens(s string) string {
|
|
| 12 |
+ re := regexp.MustCompile("\\\\;")
|
|
| 13 |
+ ret := re.ReplaceAll([]byte(s), []byte(";"))
|
|
| 14 |
+ |
|
| 15 |
+ re = regexp.MustCompile("\\\\\\(")
|
|
| 16 |
+ ret = re.ReplaceAll([]byte(ret), []byte("("))
|
|
| 17 |
+ |
|
| 18 |
+ re = regexp.MustCompile("\\\\\\)")
|
|
| 19 |
+ ret = re.ReplaceAll([]byte(ret), []byte(")"))
|
|
| 20 |
+ |
|
| 21 |
+ re = regexp.MustCompile("\\\\\\\\")
|
|
| 22 |
+ ret = re.ReplaceAll([]byte(ret), []byte("\\"))
|
|
| 23 |
+ |
|
| 24 |
+ return string(ret) |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func regexpCheckUA(c *check.C, ua string) {
|
|
| 28 |
+ re := regexp.MustCompile("(?P<dockerUA>.+) UpstreamClient(?P<upstreamUA>.+)")
|
|
| 29 |
+ substrArr := re.FindStringSubmatch(ua) |
|
| 30 |
+ |
|
| 31 |
+ c.Assert(substrArr, check.HasLen, 3, check.Commentf("Expected 'UpstreamClient()' with upstream client UA"))
|
|
| 32 |
+ dockerUA := substrArr[1] |
|
| 33 |
+ upstreamUAEscaped := substrArr[2] |
|
| 34 |
+ |
|
| 35 |
+ // check dockerUA looks correct |
|
| 36 |
+ reDockerUA := regexp.MustCompile("^docker/[0-9A-Za-z+]")
|
|
| 37 |
+ bMatchDockerUA := reDockerUA.MatchString(dockerUA) |
|
| 38 |
+ c.Assert(bMatchDockerUA, check.Equals, true, check.Commentf("Docker Engine User-Agent malformed"))
|
|
| 39 |
+ |
|
| 40 |
+ // check upstreamUA looks correct |
|
| 41 |
+ // Expecting something like: Docker-Client/1.11.0-dev (linux) |
|
| 42 |
+ upstreamUA := unescapeBackslashSemicolonParens(upstreamUAEscaped) |
|
| 43 |
+ reUpstreamUA := regexp.MustCompile("^\\(Docker-Client/[0-9A-Za-z+]")
|
|
| 44 |
+ bMatchUpstreamUA := reUpstreamUA.MatchString(upstreamUA) |
|
| 45 |
+ c.Assert(bMatchUpstreamUA, check.Equals, true, check.Commentf("(Upstream) Docker Client User-Agent malformed"))
|
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+// TestUserAgentPassThroughOnPull verifies that when an image is pulled from |
|
| 49 |
+// a registry, the registry should see a User-Agent string of the form |
|
| 50 |
+// [docker engine UA] UptreamClientSTREAM-CLIENT([client UA]) |
|
| 51 |
+func (s *DockerRegistrySuite) TestUserAgentPassThroughOnPull(c *check.C) {
|
|
| 52 |
+ reg, err := newTestRegistry(c) |
|
| 53 |
+ c.Assert(err, check.IsNil) |
|
| 54 |
+ expectUpstreamUA := false |
|
| 55 |
+ |
|
| 56 |
+ reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) {
|
|
| 57 |
+ w.WriteHeader(404) |
|
| 58 |
+ var ua string |
|
| 59 |
+ for k, v := range r.Header {
|
|
| 60 |
+ if k == "User-Agent" {
|
|
| 61 |
+ ua = v[0] |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ c.Assert(ua, check.Not(check.Equals), "", check.Commentf("No User-Agent found in request"))
|
|
| 65 |
+ if r.URL.Path == "/v2/busybox/manifests/latest" {
|
|
| 66 |
+ if expectUpstreamUA {
|
|
| 67 |
+ regexpCheckUA(c, ua) |
|
| 68 |
+ } |
|
| 69 |
+ } |
|
| 70 |
+ }) |
|
| 71 |
+ |
|
| 72 |
+ repoName := fmt.Sprintf("%s/busybox", reg.hostport)
|
|
| 73 |
+ err = s.d.Start("--insecure-registry", reg.hostport, "--disable-legacy-registry=true")
|
|
| 74 |
+ c.Assert(err, check.IsNil) |
|
| 75 |
+ |
|
| 76 |
+ dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport))
|
|
| 77 |
+ c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
|
|
| 78 |
+ defer cleanup() |
|
| 79 |
+ |
|
| 80 |
+ s.d.Cmd("build", "--file", dockerfileName, ".")
|
|
| 81 |
+ |
|
| 82 |
+ s.d.Cmd("run", repoName)
|
|
| 83 |
+ s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport)
|
|
| 84 |
+ s.d.Cmd("tag", "busybox", repoName)
|
|
| 85 |
+ s.d.Cmd("push", repoName)
|
|
| 86 |
+ |
|
| 87 |
+ expectUpstreamUA = true |
|
| 88 |
+ s.d.Cmd("pull", repoName)
|
|
| 89 |
+} |