cli/command: add WithUserAgent option

Add support to the `cli/command` package to accept a custom User
Agent to pass to the underlying client.

This is used as the `UpstreamClient` portion of the `User-Agent`
when the Moby daemon makes requests.

For example, pushing and pulling images with Compose might result
in the registry seeing a `User-Agent` value of:

```
docker/24.0.7 go/go1.20.10 git-commit/311b9ff kernel/6.5.13-linuxkit os/linux arch/arm64 UpstreamClient(docker-cli-plugin-compose/v2.24.0)
```

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Milas Bowman
2023-09-20 14:36:55 -04:00
committed by Sebastiaan van Stijn
parent 8fbb70ae56
commit 048e931b42
4 changed files with 44 additions and 5 deletions

View File

@ -77,6 +77,7 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config
initTimeout time.Duration
userAgent string
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
@ -312,10 +313,10 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
if err != nil {
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
}
return newAPIClientFromEndpoint(endpoint, configFile)
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
}
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
opts, err := ep.ClientOpts()
if err != nil {
return nil, err
@ -330,7 +331,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
if withCustomHeaders != nil {
opts = append(opts, withCustomHeaders)
}
opts = append(opts, client.WithUserAgent(UserAgent()))
opts = append(opts, extraOpts...)
return client.NewClientWithOpts(opts...)
}
@ -551,7 +552,8 @@ func (cli *DockerCli) initialize() error {
return
}
if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
return
}
}
@ -598,6 +600,7 @@ func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(),
WithStandardStreams(),
WithUserAgent(UserAgent()),
}
ops = append(defaultOps, ops...)
@ -621,7 +624,7 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
}
}
// UserAgent returns the user agent string used for making API requests
// UserAgent returns the default user agent string used for making API requests.
func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
}

View File

@ -3,6 +3,7 @@ package command
import (
"context"
"encoding/csv"
"errors"
"fmt"
"io"
"net/http"
@ -236,3 +237,14 @@ func withCustomHeadersFromEnv() (client.Opt, error) {
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env), nil
}
// WithUserAgent configures the User-Agent string for cli HTTP requests.
func WithUserAgent(userAgent string) CLIOption {
return func(cli *DockerCli) error {
if userAgent == "" {
return errors.New("user agent cannot be blank")
}
cli.userAgent = userAgent
return nil
}
}

View File

@ -373,3 +373,26 @@ func TestSetGoDebug(t *testing.T) {
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
})
}
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithUserAgent("fake-agent/0.0.1"),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}

View File

@ -53,6 +53,7 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index)
}
// FIXME(thaJeztah): this should use the userAgent as configured on the dockerCLI.
return registryclient.NewRegistryClient(resolver, command.UserAgent(), allowInsecure)
}