Skip to content

Commit 7c586d4

Browse files
authored
Implement better user agent support (#837)
* Expose transport.NewUserAgent This allows you to supply a customer user agent string that will be prepended before the go-containerregistry portion. This uses go module build info to determine the version of go-containerregistry that you have pulled in, which will be included in the user agent as "go-containerregistry/${VERSION}". If we cannot get the version, this is just "go-containerregistry". * Expose remote.WithUserAgent * Expose crane.WithUserAgent * Supply useragent from crane CLI
1 parent 5f1c4b2 commit 7c586d4

File tree

9 files changed

+115
-26
lines changed

9 files changed

+115
-26
lines changed

cmd/crane/cmd/root.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
package cmd
1414

1515
import (
16+
"fmt"
1617
"net/http"
1718
"os"
19+
"path/filepath"
1820

1921
"github.com/docker/cli/cli/config"
2022
"github.com/google/go-containerregistry/pkg/crane"
@@ -49,6 +51,13 @@ func New(use, short string, options []crane.Option) *cobra.Command {
4951
if insecure {
5052
options = append(options, crane.Insecure)
5153
}
54+
if Version != "" {
55+
binary := "crane"
56+
if len(os.Args[0]) != 0 {
57+
binary = filepath.Base(os.Args[0])
58+
}
59+
options = append(options, crane.WithUserAgent(fmt.Sprintf("%s/%s", binary, Version)))
60+
}
5261

5362
options = append(options, crane.WithPlatform(platform.platform))
5463

cmd/crane/cmd/version.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ import (
1111
// -ldflags="-X 'github.com/google/go-containerregistry/cmd/crane/cmd.Version=$TAG'"
1212
var Version string
1313

14+
func init() {
15+
if Version == "" {
16+
i, ok := debug.ReadBuildInfo()
17+
if !ok {
18+
return
19+
}
20+
Version = i.Main.Version
21+
}
22+
}
23+
1424
// NewCmdVersion creates a new cobra.Command for the version subcommand.
1525
func NewCmdVersion() *cobra.Command {
1626
return &cobra.Command{
@@ -23,14 +33,10 @@ This could also be the go module version, if built with go modules (often "(deve
2333
Args: cobra.NoArgs,
2434
Run: func(_ *cobra.Command, _ []string) {
2535
if Version == "" {
26-
i, ok := debug.ReadBuildInfo()
27-
if !ok {
28-
fmt.Println("could not determine build information")
29-
return
30-
}
31-
Version = i.Main.Version
36+
fmt.Println("could not determine build information")
37+
} else {
38+
fmt.Println(Version)
3239
}
33-
fmt.Println(Version)
3440
},
3541
}
3642
}

pkg/crane/options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,11 @@ func WithAuth(auth authn.Authenticator) Option {
8989
o.remote[0] = remote.WithAuth(auth)
9090
}
9191
}
92+
93+
// WithUserAgent adds the given string to the User-Agent header for any HTTP
94+
// requests.
95+
func WithUserAgent(ua string) Option {
96+
return func(o *options) {
97+
o.remote = append(o.remote, remote.WithUserAgent(ua))
98+
}
99+
}

pkg/v1/remote/options.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type options struct {
3535
platform v1.Platform
3636
context context.Context
3737
jobs int
38+
userAgent string
3839
}
3940

4041
var defaultPlatform = v1.Platform{
@@ -80,6 +81,11 @@ func makeOptions(target authn.Resource, opts ...Option) (*options, error) {
8081
// Wrap the transport in something that can retry network flakes.
8182
o.transport = transport.NewRetry(o.transport)
8283

84+
// Wrap this last to prevent transport.New from double-wrapping.
85+
if o.userAgent != "" {
86+
o.transport = transport.NewUserAgent(o.transport, o.userAgent)
87+
}
88+
8389
return o, nil
8490
}
8591

@@ -156,3 +162,14 @@ func WithJobs(jobs int) Option {
156162
return nil
157163
}
158164
}
165+
166+
// WithUserAgent adds the given string to the User-Agent header for any HTTP
167+
// requests. This header will also include "go-containerregistry/${version}".
168+
//
169+
// If you want to completely overwrite the User-Agent header, use WithTransport.
170+
func WithUserAgent(ua string) Option {
171+
return func(o *options) error {
172+
o.userAgent = ua
173+
return nil
174+
}
175+
}

pkg/v1/remote/transport/basic.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,5 @@ func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
5858
}
5959
}
6060
}
61-
in.Header.Set("User-Agent", transportName)
6261
return bt.inner.RoundTrip(in)
6362
}

pkg/v1/remote/transport/bearer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
233233
v := url.Values{}
234234
v.Set("scope", strings.Join(bt.scopes, " "))
235235
v.Set("service", bt.service)
236-
v.Set("client_id", transportName)
236+
v.Set("client_id", defaultUserAgent)
237237
if auth.IdentityToken != "" {
238238
v.Set("grant_type", "refresh_token")
239239
v.Set("refresh_token", auth.IdentityToken)

pkg/v1/remote/transport/transport.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,16 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic
5555
return nil, err
5656
}
5757

58-
// Wrap the given transport in transports that use an appropriate scheme,
59-
// (based on the ping response) and set the user agent.
60-
t = &useragentTransport{
61-
inner: &schemeTransport{
62-
scheme: pr.scheme,
63-
registry: reg,
64-
inner: t,
65-
},
58+
// Wrap t with a useragent transport unless we already have one.
59+
if _, ok := t.(*userAgentTransport); !ok {
60+
t = NewUserAgent(t, "")
61+
}
62+
63+
// Wrap t in a transport that selects the appropriate scheme based on the ping response.
64+
t = &schemeTransport{
65+
scheme: pr.scheme,
66+
registry: reg,
67+
inner: t,
6668
}
6769

6870
switch pr.challenge.Canonical() {

pkg/v1/remote/transport/transport_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ func TestTransportSelectionAnonymous(t *testing.T) {
6464
if got, want := recorded.URL.Scheme, "https"; got != want {
6565
t.Errorf("wrong scheme, want %s got %s", want, got)
6666
}
67-
if want, got := recorded.Header.Get("User-Agent"), transportName; want != got {
68-
t.Errorf("wrong useragent, want %s got %s", want, got)
69-
}
7067
}
7168

7269
func TestTransportSelectionBasic(t *testing.T) {

pkg/v1/remote/transport/useragent.go

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,70 @@
1414

1515
package transport
1616

17-
import "net/http"
17+
import (
18+
"fmt"
19+
"net/http"
20+
"runtime/debug"
21+
)
1822

1923
const (
20-
transportName = "go-containerregistry"
24+
defaultUserAgent = "go-containerregistry"
25+
moduleName = "github.com/google/go-containerregistry"
2126
)
2227

23-
type useragentTransport struct {
24-
// Wrapped by useragentTransport.
28+
var ggcrVersion = defaultUserAgent
29+
30+
type userAgentTransport struct {
2531
inner http.RoundTripper
32+
ua string
33+
}
34+
35+
func init() {
36+
if v := version(); v != "" {
37+
ggcrVersion = fmt.Sprintf("%s/%s", defaultUserAgent, v)
38+
}
39+
}
40+
41+
func version() string {
42+
info, ok := debug.ReadBuildInfo()
43+
if !ok {
44+
return ""
45+
}
46+
47+
// Happens for crane and gcrane.
48+
if info.Main.Path == moduleName {
49+
return info.Main.Version
50+
}
51+
52+
// Anything else.
53+
for _, dep := range info.Deps {
54+
if dep.Path == moduleName {
55+
return dep.Version
56+
}
57+
}
58+
59+
return ""
60+
}
61+
62+
// NewUserAgent returns an http.Roundtripper that sets the user agent to
63+
// The provided string plus additional go-containerregistry information,
64+
// e.g. if provided "crane/v0.1.4" and this modules was built at v0.1.4:
65+
//
66+
// User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4
67+
func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper {
68+
if ua == "" {
69+
ua = ggcrVersion
70+
} else {
71+
ua = fmt.Sprintf("%s %s", ua, ggcrVersion)
72+
}
73+
return &userAgentTransport{
74+
inner: inner,
75+
ua: ua,
76+
}
2677
}
2778

2879
// RoundTrip implements http.RoundTripper
29-
func (ut *useragentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
30-
in.Header.Set("User-Agent", transportName)
80+
func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
81+
in.Header.Set("User-Agent", ut.ua)
3182
return ut.inner.RoundTrip(in)
3283
}

0 commit comments

Comments
 (0)