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>
This commit is contained in:
Aaron Lehmann 2016-03-18 14:42:40 -07:00
parent 278d3962a8
commit c44e7a3e63
17 changed files with 109 additions and 70 deletions

View File

@ -140,7 +140,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
customHeaders["User-Agent"] = clientUserAgent()
verStr := api.DefaultVersion.String()
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
@ -209,3 +209,7 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
Transport: tr,
}, nil
}
func clientUserAgent() string {
return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
}

View File

@ -23,7 +23,6 @@ import (
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/distribution"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/pkg/jsonmessage"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/reference"
@ -152,7 +151,7 @@ func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, aut
}
// Skip configuration headers since request is not going to Docker daemon
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(""), http.Header{})
modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,

View File

@ -1,9 +1,11 @@
package build
import (
"io"
"github.com/docker/docker/builder"
"github.com/docker/engine-api/types"
"io"
"golang.org/x/net/context"
)
// Backend abstracts an image builder whose only purpose is to build an image referenced by an imageID.
@ -14,5 +16,5 @@ type Backend interface {
// by the caller.
//
// TODO: make this return a reference instead of string
Build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error)
Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error)
}

View File

@ -171,7 +171,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
closeNotifier = notifier.CloseNotify()
}
imgID, err := br.backend.Build(buildOptions,
imgID, err := br.backend.Build(ctx, buildOptions,
builder.DockerIgnoreContext{ModifiableContext: context},
stdout, stderr, out,
closeNotifier)

View File

@ -39,6 +39,6 @@ type importExportBackend interface {
type registryBackend interface {
PullImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
SearchRegistryForImages(term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
SearchRegistryForImages(ctx context.Context, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
}

View File

@ -228,7 +228,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
w.Header().Set("Content-Type", "application/json")
if err := s.backend.PushImage(ref, metaHeaders, authConfig, output); err != nil {
if err := s.backend.PushImage(ctx, ref, metaHeaders, authConfig, output); err != nil {
if !output.Flushed() {
return err
}
@ -373,7 +373,7 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
headers[k] = v
}
}
query, err := s.backend.SearchRegistryForImages(r.Form.Get("term"), config, headers)
query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("term"), config, headers)
if err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/events"
"github.com/docker/engine-api/types/filters"
"golang.org/x/net/context"
)
// Backend is the methods that need to be implemented to provide
@ -13,5 +14,5 @@ type Backend interface {
SystemVersion() types.Version
SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
UnsubscribeFromEvents(chan interface{})
AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error)
}

View File

@ -115,7 +115,7 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
if err != nil {
return err
}
status, token, err := s.backend.AuthenticateToRegistry(config)
status, token, err := s.backend.AuthenticateToRegistry(ctx, config)
if err != nil {
return err
}

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/docker/reference"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"golang.org/x/net/context"
)
const (
@ -109,7 +110,7 @@ type Backend interface {
// Tag an image with newTag
TagImage(newTag reference.Named, imageName string) error
// Pull tells Docker to pull image referenced by `name`.
PullOnBuild(name string, authConfigs map[string]types.AuthConfig, output io.Writer) (Image, error)
PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (Image, error)
// ContainerAttach attaches to container.
ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool) error
// ContainerCreate creates a new Docker container and returns potential warnings

View File

@ -17,6 +17,7 @@ import (
"github.com/docker/docker/reference"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"golang.org/x/net/context"
)
var validCommitCommands = map[string]bool{
@ -52,8 +53,9 @@ type Builder struct {
Stderr io.Writer
Output io.Writer
docker builder.Backend
context builder.Context
docker builder.Backend
context builder.Context
clientCtx context.Context
dockerfile *parser.Node
runConfig *container.Config // runconfig for cmd, run, entrypoint etc.
@ -86,7 +88,7 @@ func NewBuildManager(b builder.Backend) (bm *BuildManager) {
// NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
// If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
// will be read from the Context passed to Build().
func NewBuilder(config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
if config == nil {
config = new(types.ImageBuildOptions)
}
@ -94,6 +96,7 @@ func NewBuilder(config *types.ImageBuildOptions, backend builder.Backend, contex
config.BuildArgs = make(map[string]string)
}
b = &Builder{
clientCtx: clientCtx,
options: config,
Stdout: os.Stdout,
Stderr: os.Stderr,
@ -158,8 +161,8 @@ func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
}
// Build creates a NewBuilder, which builds the image.
func (bm *BuildManager) Build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) {
b, err := NewBuilder(config, bm.backend, context, nil)
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) {
b, err := NewBuilder(clientCtx, config, bm.backend, context, nil)
if err != nil {
return "", err
}
@ -291,7 +294,7 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con
}
}
b, err := NewBuilder(nil, nil, nil, nil)
b, err := NewBuilder(context.Background(), nil, nil, nil, nil)
if err != nil {
return nil, err
}

View File

@ -206,7 +206,7 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
// TODO: shouldn't we error out if error is different from "not found" ?
}
if image == nil {
image, err = b.docker.PullOnBuild(name, b.options.AuthConfigs, b.Output)
image, err = b.docker.PullOnBuild(b.clientCtx, name, b.options.AuthConfigs, b.Output)
if err != nil {
return err
}

View File

@ -1030,7 +1030,7 @@ func (daemon *Daemon) PullImage(ctx context.Context, ref reference.Named, metaHe
}
// PullOnBuild tells Docker to pull image referenced by `name`.
func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.AuthConfig, output io.Writer) (builder.Image, error) {
func (daemon *Daemon) PullOnBuild(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (builder.Image, error) {
ref, err := reference.ParseNamed(name)
if err != nil {
return nil, err
@ -1052,7 +1052,7 @@ func (daemon *Daemon) PullOnBuild(name string, authConfigs map[string]types.Auth
pullRegistryAuth = &resolvedConfig
}
if err := daemon.PullImage(context.Background(), ref, nil, pullRegistryAuth, output); err != nil {
if err := daemon.PullImage(ctx, ref, nil, pullRegistryAuth, output); err != nil {
return nil, err
}
return daemon.GetImage(name)
@ -1069,14 +1069,14 @@ func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error {
}
// PushImage initiates a push operation on the repository named localName.
func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
func (daemon *Daemon) PushImage(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
// Include a buffer so that slow client connections don't affect
// transfer performance.
progressChan := make(chan progress.Progress, 100)
writesDone := make(chan struct{})
ctx, cancelFunc := context.WithCancel(context.Background())
ctx, cancelFunc := context.WithCancel(ctx)
go func() {
writeDistributionProgress(cancelFunc, outStream, progressChan)
@ -1502,16 +1502,16 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
}
// AuthenticateToRegistry checks the validity of credentials in authConfig
func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) {
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(""))
func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx))
}
// SearchRegistryForImages queries the registry for images matching
// term. authConfig is used to login.
func (daemon *Daemon) SearchRegistryForImages(term string,
func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, term string,
authConfig *types.AuthConfig,
headers map[string][]string) (*registrytypes.SearchResults, error) {
return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(""), headers)
return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
}
// IsShuttingDown tells whether the daemon is shutting down or not

View File

@ -49,10 +49,10 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named) error {
tr := transport.NewTransport(
// TODO(tiborvass): was ReceiveTimeout
registry.NewTransport(tlsConfig),
registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)...,
)
client := registry.HTTPClient(tr)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return fallbackError{err: err}

View File

@ -38,10 +38,10 @@ func (p *v1Pusher) Push(ctx context.Context) error {
tr := transport.NewTransport(
// TODO(tiborvass): was NoTimeout
registry.NewTransport(tlsConfig),
registry.DockerHeaders(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)...,
registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)...,
)
client := registry.HTTPClient(tr)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(""), p.config.MetaHeaders)
v1Endpoint, err := p.endpoint.ToV1Endpoint(dockerversion.DockerUserAgent(ctx), p.config.MetaHeaders)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return fallbackError{err: err}

View File

@ -37,8 +37,6 @@ func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
// providing timeout settings and authentication support, and also verifies the
// remote API version.
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) {
upstreamUA := dockerversion.GetUserAgentFromContext(ctx)
repoName := repoInfo.FullName()
// If endpoint does not support CanonicalName, use the RemoteName instead
if endpoint.TrimHostname {
@ -59,7 +57,7 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
DisableKeepAlives: true,
}
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(upstreamUA), metaHeaders)
modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(ctx), metaHeaders)
authTransport := transport.NewTransport(base, modifiers...)
challengeManager, foundVersion, err := registry.PingV2Registry(endpoint, authTransport)

View File

@ -13,7 +13,7 @@ import (
// DockerUserAgent is the User-Agent the Docker client uses to identify itself.
// In accordance with RFC 7231 (5.5.3) is of the form:
// [docker client's UA] UpstreamClient([upstream client's UA])
func DockerUserAgent(upstreamUA string) string {
func DockerUserAgent(ctx context.Context) string {
httpVersion := make([]useragent.VersionInfo, 0, 6)
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: Version})
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
@ -25,6 +25,7 @@ func DockerUserAgent(upstreamUA string) string {
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
dockerUA := useragent.AppendVersions("", httpVersion...)
upstreamUA := getUserAgentFromContext(ctx)
if len(upstreamUA) > 0 {
ret := insertUpstreamUserAgent(upstreamUA, dockerUA)
return ret
@ -32,8 +33,8 @@ func DockerUserAgent(upstreamUA string) string {
return dockerUA
}
// GetUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists
func GetUserAgentFromContext(ctx context.Context) string {
// getUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists
func getUserAgentFromContext(ctx context.Context) string {
var upstreamUA string
if ctx != nil {
var ki interface{} = ctx.Value(httputils.UAStringKey)
@ -51,7 +52,7 @@ func escapeStr(s string, charsToEscape string) string {
appended := false
for _, escapeableRune := range charsToEscape {
if currRune == escapeableRune {
ret += "\\" + string(currRune)
ret += `\` + string(currRune)
appended = true
break
}
@ -67,7 +68,7 @@ func escapeStr(s string, charsToEscape string) string {
// string of the form:
// $dockerUA UpstreamClient($upstreamUA)
func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string {
charsToEscape := "();\\" //["\\", ";", "(", ")"]string
charsToEscape := `();\`
upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape)
return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped)
}

View File

@ -10,17 +10,17 @@ import (
// unescapeBackslashSemicolonParens unescapes \;()
func unescapeBackslashSemicolonParens(s string) string {
re := regexp.MustCompile("\\\\;")
re := regexp.MustCompile(`\\;`)
ret := re.ReplaceAll([]byte(s), []byte(";"))
re = regexp.MustCompile("\\\\\\(")
re = regexp.MustCompile(`\\\(`)
ret = re.ReplaceAll([]byte(ret), []byte("("))
re = regexp.MustCompile("\\\\\\)")
re = regexp.MustCompile(`\\\)`)
ret = re.ReplaceAll([]byte(ret), []byte(")"))
re = regexp.MustCompile("\\\\\\\\")
ret = re.ReplaceAll([]byte(ret), []byte("\\"))
re = regexp.MustCompile(`\\\\`)
ret = re.ReplaceAll([]byte(ret), []byte(`\`))
return string(ret)
}
@ -46,14 +46,7 @@ func regexpCheckUA(c *check.C, ua string) {
c.Assert(bMatchUpstreamUA, check.Equals, true, check.Commentf("(Upstream) Docker Client User-Agent malformed"))
}
// TestUserAgentPassThroughOnPull verifies that when an image is pulled from
// a registry, the registry should see a User-Agent string of the form
// [docker engine UA] UptreamClientSTREAM-CLIENT([client UA])
func (s *DockerRegistrySuite) TestUserAgentPassThroughOnPull(c *check.C) {
reg, err := newTestRegistry(c)
c.Assert(err, check.IsNil)
expectUpstreamUA := false
func registerUserAgentHandler(reg *testRegistry, result *string) {
reg.registerHandler("/v2/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
var ua string
@ -62,29 +55,66 @@ func (s *DockerRegistrySuite) TestUserAgentPassThroughOnPull(c *check.C) {
ua = v[0]
}
}
c.Assert(ua, check.Not(check.Equals), "", check.Commentf("No User-Agent found in request"))
if r.URL.Path == "/v2/busybox/manifests/latest" {
if expectUpstreamUA {
regexpCheckUA(c, ua)
}
}
*result = ua
})
}
repoName := fmt.Sprintf("%s/busybox", reg.hostport)
err = s.d.Start("--insecure-registry", reg.hostport, "--disable-legacy-registry=true")
// TestUserAgentPassThroughOnPull verifies that when an image is pulled from
// a registry, the registry should see a User-Agent string of the form
// [docker engine UA] UptreamClientSTREAM-CLIENT([client UA])
func (s *DockerRegistrySuite) TestUserAgentPassThrough(c *check.C) {
var (
buildUA string
pullUA string
pushUA string
loginUA string
)
buildReg, err := newTestRegistry(c)
c.Assert(err, check.IsNil)
registerUserAgentHandler(buildReg, &buildUA)
buildRepoName := fmt.Sprintf("%s/busybox", buildReg.hostport)
pullReg, err := newTestRegistry(c)
c.Assert(err, check.IsNil)
registerUserAgentHandler(pullReg, &pullUA)
pullRepoName := fmt.Sprintf("%s/busybox", pullReg.hostport)
pushReg, err := newTestRegistry(c)
c.Assert(err, check.IsNil)
registerUserAgentHandler(pushReg, &pushUA)
pushRepoName := fmt.Sprintf("%s/busybox", pushReg.hostport)
loginReg, err := newTestRegistry(c)
c.Assert(err, check.IsNil)
registerUserAgentHandler(loginReg, &loginUA)
err = s.d.Start(
"--insecure-registry", buildReg.hostport,
"--insecure-registry", pullReg.hostport,
"--insecure-registry", pushReg.hostport,
"--insecure-registry", loginReg.hostport,
"--disable-legacy-registry=true")
c.Assert(err, check.IsNil)
dockerfileName, cleanup, err := makefile(fmt.Sprintf("FROM %s/busybox", reg.hostport))
dockerfileName, cleanup1, err := makefile(fmt.Sprintf("FROM %s", buildRepoName))
c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
defer cleanup()
defer cleanup1()
s.d.Cmd("build", "--file", dockerfileName, ".")
regexpCheckUA(c, buildUA)
s.d.Cmd("run", repoName)
s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", reg.hostport)
s.d.Cmd("tag", "busybox", repoName)
s.d.Cmd("push", repoName)
s.d.Cmd("login", "-u", "richard", "-p", "testtest", "-e", "testuser@testdomain.com", loginReg.hostport)
regexpCheckUA(c, loginUA)
expectUpstreamUA = true
s.d.Cmd("pull", repoName)
s.d.Cmd("pull", pullRepoName)
regexpCheckUA(c, pullUA)
dockerfileName, cleanup2, err := makefile(`FROM scratch
ENV foo bar`)
c.Assert(err, check.IsNil, check.Commentf("Unable to create test dockerfile"))
defer cleanup2()
s.d.Cmd("build", "-t", pushRepoName, "--file", dockerfileName, ".")
s.d.Cmd("push", pushRepoName)
regexpCheckUA(c, pushUA)
}