Skip to content

Commit

Permalink
Implement better user agent support (#837)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jonjohnsonjr committed Dec 1, 2020
1 parent 5f1c4b2 commit 7c586d4
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 26 deletions.
9 changes: 9 additions & 0 deletions cmd/crane/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
package cmd

import (
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/docker/cli/cli/config"
"github.com/google/go-containerregistry/pkg/crane"
Expand Down Expand Up @@ -49,6 +51,13 @@ func New(use, short string, options []crane.Option) *cobra.Command {
if insecure {
options = append(options, crane.Insecure)
}
if Version != "" {
binary := "crane"
if len(os.Args[0]) != 0 {
binary = filepath.Base(os.Args[0])
}
options = append(options, crane.WithUserAgent(fmt.Sprintf("%s/%s", binary, Version)))
}

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

Expand Down
20 changes: 13 additions & 7 deletions cmd/crane/cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import (
// -ldflags="-X 'github.com/google/go-containerregistry/cmd/crane/cmd.Version=$TAG'"
var Version string

func init() {
if Version == "" {
i, ok := debug.ReadBuildInfo()
if !ok {
return
}
Version = i.Main.Version
}
}

// NewCmdVersion creates a new cobra.Command for the version subcommand.
func NewCmdVersion() *cobra.Command {
return &cobra.Command{
Expand All @@ -23,14 +33,10 @@ This could also be the go module version, if built with go modules (often "(deve
Args: cobra.NoArgs,
Run: func(_ *cobra.Command, _ []string) {
if Version == "" {
i, ok := debug.ReadBuildInfo()
if !ok {
fmt.Println("could not determine build information")
return
}
Version = i.Main.Version
fmt.Println("could not determine build information")
} else {
fmt.Println(Version)
}
fmt.Println(Version)
},
}
}
8 changes: 8 additions & 0 deletions pkg/crane/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@ func WithAuth(auth authn.Authenticator) Option {
o.remote[0] = remote.WithAuth(auth)
}
}

// WithUserAgent adds the given string to the User-Agent header for any HTTP
// requests.
func WithUserAgent(ua string) Option {
return func(o *options) {
o.remote = append(o.remote, remote.WithUserAgent(ua))
}
}
17 changes: 17 additions & 0 deletions pkg/v1/remote/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type options struct {
platform v1.Platform
context context.Context
jobs int
userAgent string
}

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

// Wrap this last to prevent transport.New from double-wrapping.
if o.userAgent != "" {
o.transport = transport.NewUserAgent(o.transport, o.userAgent)
}

return o, nil
}

Expand Down Expand Up @@ -156,3 +162,14 @@ func WithJobs(jobs int) Option {
return nil
}
}

// WithUserAgent adds the given string to the User-Agent header for any HTTP
// requests. This header will also include "go-containerregistry/${version}".
//
// If you want to completely overwrite the User-Agent header, use WithTransport.
func WithUserAgent(ua string) Option {
return func(o *options) error {
o.userAgent = ua
return nil
}
}
1 change: 0 additions & 1 deletion pkg/v1/remote/transport/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,5 @@ func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) {
}
}
}
in.Header.Set("User-Agent", transportName)
return bt.inner.RoundTrip(in)
}
2 changes: 1 addition & 1 deletion pkg/v1/remote/transport/bearer.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
v := url.Values{}
v.Set("scope", strings.Join(bt.scopes, " "))
v.Set("service", bt.service)
v.Set("client_id", transportName)
v.Set("client_id", defaultUserAgent)
if auth.IdentityToken != "" {
v.Set("grant_type", "refresh_token")
v.Set("refresh_token", auth.IdentityToken)
Expand Down
18 changes: 10 additions & 8 deletions pkg/v1/remote/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,16 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic
return nil, err
}

// Wrap the given transport in transports that use an appropriate scheme,
// (based on the ping response) and set the user agent.
t = &useragentTransport{
inner: &schemeTransport{
scheme: pr.scheme,
registry: reg,
inner: t,
},
// Wrap t with a useragent transport unless we already have one.
if _, ok := t.(*userAgentTransport); !ok {
t = NewUserAgent(t, "")
}

// Wrap t in a transport that selects the appropriate scheme based on the ping response.
t = &schemeTransport{
scheme: pr.scheme,
registry: reg,
inner: t,
}

switch pr.challenge.Canonical() {
Expand Down
3 changes: 0 additions & 3 deletions pkg/v1/remote/transport/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ func TestTransportSelectionAnonymous(t *testing.T) {
if got, want := recorded.URL.Scheme, "https"; got != want {
t.Errorf("wrong scheme, want %s got %s", want, got)
}
if want, got := recorded.Header.Get("User-Agent"), transportName; want != got {
t.Errorf("wrong useragent, want %s got %s", want, got)
}
}

func TestTransportSelectionBasic(t *testing.T) {
Expand Down
63 changes: 57 additions & 6 deletions pkg/v1/remote/transport/useragent.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,70 @@

package transport

import "net/http"
import (
"fmt"
"net/http"
"runtime/debug"
)

const (
transportName = "go-containerregistry"
defaultUserAgent = "go-containerregistry"
moduleName = "github.com/google/go-containerregistry"
)

type useragentTransport struct {
// Wrapped by useragentTransport.
var ggcrVersion = defaultUserAgent

type userAgentTransport struct {
inner http.RoundTripper
ua string
}

func init() {
if v := version(); v != "" {
ggcrVersion = fmt.Sprintf("%s/%s", defaultUserAgent, v)
}
}

func version() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}

// Happens for crane and gcrane.
if info.Main.Path == moduleName {
return info.Main.Version
}

// Anything else.
for _, dep := range info.Deps {
if dep.Path == moduleName {
return dep.Version
}
}

return ""
}

// NewUserAgent returns an http.Roundtripper that sets the user agent to
// The provided string plus additional go-containerregistry information,
// e.g. if provided "crane/v0.1.4" and this modules was built at v0.1.4:
//
// User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4
func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper {
if ua == "" {
ua = ggcrVersion
} else {
ua = fmt.Sprintf("%s %s", ua, ggcrVersion)
}
return &userAgentTransport{
inner: inner,
ua: ua,
}
}

// RoundTrip implements http.RoundTripper
func (ut *useragentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
in.Header.Set("User-Agent", transportName)
func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) {
in.Header.Set("User-Agent", ut.ua)
return ut.inner.RoundTrip(in)
}

0 comments on commit 7c586d4

Please sign in to comment.