Skip to content

Conversation

@milas
Copy link
Contributor

@milas milas commented Sep 20, 2023

How I did it
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)

See also:

- How to verify it
Build a CLI plugin against latest HEAD, update its SchemaVersion to 0.2.0, and inspect network traffic.

- Description for the changelog

Go SDK: cli/command: add `WithUserAgent` option

- A picture of a cute animal (not mandatory but encouraged)
several baby skunks with bushy tails

Comment on lines 44 to 45
case "0.2.0":
if meta.Name == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be careful with this one (yes, I've been looking at that gnarly "must be 0.1.0" as well), but if we'd ship a plugin with 0.2.0, it would mean it cannot be used with any CLI other than docker 25.0.0

Which also means that anyone installing an older version of the deb/rpm packages will now be broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gah! You're absolutely right.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the 0.1.0 threw me for a loop and made me think "oh, semver, I should update this!"

I suppose from the perspective of a JSON-ish API, field additions are typically acceptable from a compatibility standpoint. How do you feel if I rollback the SchemaVersion part? Keep it as 0.1.0 but start sending Name.

Copy link
Contributor Author

@milas milas Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible alternative: change the signature of plugin.Run() to be (name, meta, factoryFn). It avoids changing Metadata at all at the expense of breaking the API. (That will guarantee that all plugins start reporting UA once they upgrade at least...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think we can change the code to remove the fixed requirement (we can set a minimum version of v0.1.0)

That won't fix existing cli's, but at least removes the weird bit for future.

Other than that, I think it's fine to add new fields, which means that we can detect if new fields are set (older CLI's would ignore them), and we could use that for feature detection.

(possibly a field that indicates what features are supported, if that helps avoid ambiguity between "empty field" and "feature supported but some field is empty")

dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
initTimeout time.Duration
userAgent string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DockerCli uses that helper for API client construction! But DockerCli also lazily creates RegistryClient and NotaryClient instances and passes along a user agent to them, so it does need it.

I agree that we might want to add a WithAPIClientOption passthrough for future tweaks to the API client without needing 2x PRs each time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! Right, yeah, I hate it that we also re-invent our own "registry" client here (and I think all that 💩 is for, erm, mostly legacy stuff).

I'll have a closer look if I can come up with some solutions (otherwise this looks fine to me). Things I still have my eyes on (not related to your changes per-se, but a bit of a sore point already);

  • not a fan of requiring a fixed argument to pass around "user-agent" everywhere
  • ☝️ even more if that is "hard-coded" (hard-coded, as in: a function-call that is hard-coded)
  • ☝️ at least your PR improves that part

But indeed persisting in 2 separate locations isn't ideal; also considering; is this something we should pass over the context (at least that would allow passing this around without requiring it to be passed as a dedicated argument that "somehow" needs to find its way somewhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarification: when you say context, do you mean Docker context or Go context.Context?

It could canonically live inside the API client, but then that interface would need to gain a getter so that DockerCli could read it back out to pass to registry & notary client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh! Sorry, somehow missed your last comment (also still need to look at this PR again 🙈)

I meant context.Context and to (ab)use the context to pass this information, which would allow functions that need a user-agent to get access to it (which could potentially avoid having to punch-through a UserAgent argument to many places.

I haven't given that idea much thought yet though; I was considering that approach for API version (but for API version it may be more "clear-cut" that the API version to use would usually be "per request", and as such "per context"; for User-Agent that's more of a "grey area" I guess 😂

@milas milas requested a review from thaJeztah September 25, 2023 14:28
@thaJeztah thaJeztah added this to the 25.0.0 milestone Nov 6, 2023
Comment on lines 56 to 61
var dockerCliOpts []command.DockerCliOption
if ua, ok := userAgent(meta); ok {
dockerCliOpts = append(dockerCliOpts, command.WithUserAgent(ua))
}

dockerCli, err := command.NewDockerCli(dockerCliOpts...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay (again), this one dropped off my list.

I was just wondering; do we actually need the Metadata to have the new field? Would it also work if the plugin would use dockerCli.Apply() and set the user agent?

Looking at compose; https://github.com/docker/compose/blob/e5cd265abbdd9d67d76558d3b8d2e7fec68b7573/cmd/main.go#L36-L39

It gets the dockerCLI passed; which (with the option you added in this PR) could use that function to set a custom user-agent;

func pluginMain() {
	plugin.Run(func(dockerCli command.Cli) *cobra.Command {
		_ = dockerCli.Apply(command.WithUserAgent("whatever"))

The net-result would be the same, but without the Metadata as intermediate (because that one also gets used for the JSON response, where it's not used?)

@codecov-commenter
Copy link

codecov-commenter commented Jan 4, 2024

Codecov Report

❌ Patch coverage is 69.23077% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cli/command/cli_options.go 57.14% 2 Missing and 1 partial ⚠️
cli/command/cli.go 83.33% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@milas milas requested a review from thaJeztah January 4, 2024 20:07
@milas
Copy link
Contributor Author

milas commented Jan 4, 2024

@thaJeztah PTAL - I refactored and there's no changes in the plugin metadata needed any longer.

I'm using the plugin.Name() now, which is the Cobra subcommand (e.g. compose, buildx). The UA will have docker-cli-plugin- prepended.

[See updated PR description for more details]

@thaJeztah thaJeztah modified the milestones: 25.0.0, 26.0.0 Jan 19, 2024
@vvoland vvoland modified the milestones: 26.0.0, 27.0.0 Mar 14, 2024
@vvoland vvoland modified the milestones: 27.0.0, v-future Jun 20, 2024
@thaJeztah thaJeztah modified the milestones: v-future, 29.0.0 Sep 15, 2025
@thaJeztah thaJeztah changed the title cli-plugins: include plugin metadata in User-Agent cli/command: add WithUserAgent option Sep 24, 2025
@thaJeztah thaJeztah added impact/changelog area/go-sdk Changes affecting the Go SDK impact/go-sdk Noteworthy (compatibility changes) in the Go SDK labels Sep 24, 2025
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 <[email protected]>
Signed-off-by: Sebastiaan van Stijn <[email protected]>
Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

I rewrote this to take advantage of new options we have on plugin.Run

@vvoland vvoland merged commit ed7908e into docker:master Sep 24, 2025
84 of 86 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/go-sdk Changes affecting the Go SDK area/plugins impact/changelog impact/go-sdk Noteworthy (compatibility changes) in the Go SDK kind/enhancement status/2-code-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants