Merge pull request #21373 from aaronlehmann/client-user-agent-registry-operations

Pass upstream client's user agent through to registry on operations beyond pulls
This commit is contained in:
Arnaud Porterie 2016-03-21 21:49:48 -07:00
commit 9f327b4c28
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)
}