From eb9a5392bcbcf140bfd29c9ec2ba29e7100a27a4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 1 Jun 2023 14:05:20 +0200 Subject: [PATCH 1/5] dockerversion: add a basic unit-test Signed-off-by: Sebastiaan van Stijn --- dockerversion/useragent_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dockerversion/useragent_test.go diff --git a/dockerversion/useragent_test.go b/dockerversion/useragent_test.go new file mode 100644 index 0000000000..86d5004fb7 --- /dev/null +++ b/dockerversion/useragent_test.go @@ -0,0 +1,24 @@ +package dockerversion + +import ( + "context" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestDockerUserAgent(t *testing.T) { + t.Run("daemon user-agent", func(t *testing.T) { + ua := DockerUserAgent(context.TODO()) + expected := getDaemonUserAgent() + assert.Check(t, is.Equal(ua, expected)) + }) + + t.Run("daemon user-agent with upstream", func(t *testing.T) { + ctx := context.WithValue(context.TODO(), UAStringKey{}, "Magic-Client/1.2.3 (linux)") + ua := DockerUserAgent(ctx) + expected := getDaemonUserAgent() + ` UpstreamClient(Magic-Client/1.2.3 \(linux\))` + assert.Check(t, is.Equal(ua, expected)) + }) +} From ff40d2d787d2fb8c8d83612cb02aa32d70e6a241 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 1 Jun 2023 13:38:45 +0200 Subject: [PATCH 2/5] dockerversion: simplify escapeStr() Use a const for the characters to escape, instead of implementing this as a generic escaping function. Signed-off-by: Sebastiaan van Stijn --- dockerversion/useragent.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dockerversion/useragent.go b/dockerversion/useragent.go index 7fc3b69a75..bd65800e99 100644 --- a/dockerversion/useragent.go +++ b/dockerversion/useragent.go @@ -69,8 +69,10 @@ func getUserAgentFromContext(ctx context.Context) string { return upstreamUA } +const charsToEscape = `();\` + // escapeStr returns s with every rune in charsToEscape escaped by a backslash -func escapeStr(s string, charsToEscape string) string { +func escapeStr(s string) string { var ret string for _, currRune := range s { appended := false @@ -93,7 +95,5 @@ func escapeStr(s string, charsToEscape string) string { // // $dockerUA UpstreamClient($upstreamUA) func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string { - charsToEscape := `();\` - upstreamUAEscaped := escapeStr(upstreamUA, charsToEscape) - return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, upstreamUAEscaped) + return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, escapeStr(upstreamUA)) } From 9a1f2e6d7cd68d872bdac2cf27cbaad37c9fcd58 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 1 Jun 2023 13:50:30 +0200 Subject: [PATCH 3/5] dockerversion: remove insertUpstreamUserAgent() It was not really "inserting" anything, just formatting and appending. Simplify this by changing this in to a `getUpstreamUserAgent()` function which returns the upstream User-Agent (if any) into a `UpstreamClient()`. Signed-off-by: Sebastiaan van Stijn --- dockerversion/useragent.go | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/dockerversion/useragent.go b/dockerversion/useragent.go index bd65800e99..232a28acc9 100644 --- a/dockerversion/useragent.go +++ b/dockerversion/useragent.go @@ -18,11 +18,11 @@ type UAStringKey struct{} // // [docker client's UA] UpstreamClient([upstream client's UA]) func DockerUserAgent(ctx context.Context) string { - daemonUA := getDaemonUserAgent() - if upstreamUA := getUserAgentFromContext(ctx); len(upstreamUA) > 0 { - return insertUpstreamUserAgent(upstreamUA, daemonUA) + ua := getDaemonUserAgent() + if upstreamUA := getUpstreamUserAgent(ctx); upstreamUA != "" { + ua += " " + upstreamUA } - return daemonUA + return ua } var ( @@ -57,16 +57,23 @@ func getDaemonUserAgent() string { return daemonUA } -// getUserAgentFromContext returns the previously saved user-agent context stored in ctx, if one exists -func getUserAgentFromContext(ctx context.Context) string { +// getUpstreamUserAgent returns the previously saved user-agent context stored +// in ctx, if one exists, and formats it as: +// +// UpstreamClient() +// +// It returns an empty string if no user-agent is present in the context. +func getUpstreamUserAgent(ctx context.Context) string { var upstreamUA string if ctx != nil { - var ki interface{} = ctx.Value(UAStringKey{}) - if ki != nil { + if ki := ctx.Value(UAStringKey{}); ki != nil { upstreamUA = ctx.Value(UAStringKey{}).(string) } } - return upstreamUA + if upstreamUA == "" { + return "" + } + return fmt.Sprintf("UpstreamClient(%s)", escapeStr(upstreamUA)) } const charsToEscape = `();\` @@ -89,11 +96,3 @@ func escapeStr(s string) string { } return ret } - -// insertUpstreamUserAgent adds the upstream client useragent to create a user-agent -// string of the form: -// -// $dockerUA UpstreamClient($upstreamUA) -func insertUpstreamUserAgent(upstreamUA string, dockerUA string) string { - return fmt.Sprintf("%s UpstreamClient(%s)", dockerUA, escapeStr(upstreamUA)) -} From a6da1480b5197ad0d2dc7194b117d8dfe615df44 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 1 Jun 2023 14:09:50 +0200 Subject: [PATCH 4/5] dockerversion: DockerUserAgent(): allow custom versions to be passed Allow additional metadata to be passed as part of the generated User-Agent. Signed-off-by: Sebastiaan van Stijn --- dockerversion/useragent.go | 4 ++-- dockerversion/useragent_test.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/dockerversion/useragent.go b/dockerversion/useragent.go index 232a28acc9..7013a4543d 100644 --- a/dockerversion/useragent.go +++ b/dockerversion/useragent.go @@ -17,8 +17,8 @@ type UAStringKey struct{} // In accordance with RFC 7231 (5.5.3) is of the form: // // [docker client's UA] UpstreamClient([upstream client's UA]) -func DockerUserAgent(ctx context.Context) string { - ua := getDaemonUserAgent() +func DockerUserAgent(ctx context.Context, extraVersions ...useragent.VersionInfo) string { + ua := useragent.AppendVersions(getDaemonUserAgent(), extraVersions...) if upstreamUA := getUpstreamUserAgent(ctx); upstreamUA != "" { ua += " " + upstreamUA } diff --git a/dockerversion/useragent_test.go b/dockerversion/useragent_test.go index 86d5004fb7..b9fe3d2dfa 100644 --- a/dockerversion/useragent_test.go +++ b/dockerversion/useragent_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/docker/docker/pkg/useragent" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,10 +16,23 @@ func TestDockerUserAgent(t *testing.T) { assert.Check(t, is.Equal(ua, expected)) }) + t.Run("daemon user-agent custom metadata", func(t *testing.T) { + ua := DockerUserAgent(context.TODO(), useragent.VersionInfo{Name: "hello", Version: "world"}, useragent.VersionInfo{Name: "foo", Version: "bar"}) + expected := getDaemonUserAgent() + ` hello/world foo/bar` + assert.Check(t, is.Equal(ua, expected)) + }) + t.Run("daemon user-agent with upstream", func(t *testing.T) { ctx := context.WithValue(context.TODO(), UAStringKey{}, "Magic-Client/1.2.3 (linux)") ua := DockerUserAgent(ctx) expected := getDaemonUserAgent() + ` UpstreamClient(Magic-Client/1.2.3 \(linux\))` assert.Check(t, is.Equal(ua, expected)) }) + + t.Run("daemon user-agent with upstream and custom metadata", func(t *testing.T) { + ctx := context.WithValue(context.TODO(), UAStringKey{}, "Magic-Client/1.2.3 (linux)") + ua := DockerUserAgent(ctx, useragent.VersionInfo{Name: "hello", Version: "world"}, useragent.VersionInfo{Name: "foo", Version: "bar"}) + expected := getDaemonUserAgent() + ` hello/world foo/bar UpstreamClient(Magic-Client/1.2.3 \(linux\))` + assert.Check(t, is.Equal(ua, expected)) + }) } From d099e47e00ee1a34446d74561279a4fba5417ee2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 1 Jun 2023 14:16:22 +0200 Subject: [PATCH 5/5] containerd: add c8d version and storage-driver to User-Agent With this patch, the user-agent has information about the containerd-client version and the storage-driver that's used when using the containerd-integration; time="2023-06-01T11:27:07.959822887Z" level=info msg="listening on [::]:5000" go.version=go1.19.9 instance.id=53590f34-096a-4fd1-9c58-d3b8eb7e5092 service=registry version=2.8.2 ... 172.18.0.1 - - [01/Jun/2023:11:30:12 +0000] "HEAD /v2/multifoo/blobs/sha256:c7ec7661263e5e597156f2281d97b160b91af56fa1fd2cc045061c7adac4babd HTTP/1.1" 404 157 "" "docker/dev go/go1.20.4 git-commit/8d67d0c1a8 kernel/5.15.49-linuxkit-pr os/linux arch/arm64 containerd-client/1.6.21+unknown storage-driver/overlayfs UpstreamClient(Docker-Client/24.0.2 \\(linux\\))" Signed-off-by: Sebastiaan van Stijn --- daemon/containerd/resolver.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/containerd/resolver.go b/daemon/containerd/resolver.go index 97bc9780df..db44b03dff 100644 --- a/daemon/containerd/resolver.go +++ b/daemon/containerd/resolver.go @@ -8,8 +8,10 @@ import ( "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" + "github.com/containerd/containerd/version" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/dockerversion" + "github.com/docker/docker/pkg/useragent" "github.com/docker/docker/registry" "github.com/sirupsen/logrus" ) @@ -20,7 +22,7 @@ func (i *ImageService) newResolverFromAuthConfig(ctx context.Context, authConfig hosts := hostsWrapper(hostsFn, authConfig, i.registryService) headers := http.Header{} - headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx)) + headers.Set("User-Agent", dockerversion.DockerUserAgent(ctx, useragent.VersionInfo{Name: "containerd-client", Version: version.Version}, useragent.VersionInfo{Name: "storage-driver", Version: i.snapshotter})) return docker.NewResolver(docker.ResolverOptions{ Hosts: hosts,