diff --git a/cli/command/cli.go b/cli/command/cli.go index d6a2a9f04a82..026d0807a9e4 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -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 + ")" } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index b9a94fd6e217..87f8a2cc7d7c 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -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 + } +} diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index 27cf2e0610a2..7990322fdeef 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -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") +} diff --git a/cli/command/manifest/annotate.go b/cli/command/manifest/annotate.go index a881a802015d..8b592abfa8b9 100644 --- a/cli/command/manifest/annotate.go +++ b/cli/command/manifest/annotate.go @@ -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) }