diff --git a/cmd/crane/cmd/auth.go b/cmd/crane/cmd/auth.go index 5586e822c..9fe6fae1d 100644 --- a/cmd/crane/cmd/auth.go +++ b/cmd/crane/cmd/auth.go @@ -28,6 +28,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/spf13/cobra" ) @@ -39,7 +40,77 @@ func NewCmdAuth(options []crane.Option, argv ...string) *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Usage() }, } - cmd.AddCommand(NewCmdAuthGet(options, argv...), NewCmdAuthLogin(argv...), NewCmdAuthLogout(argv...)) + cmd.AddCommand(NewCmdAuthGet(options, argv...), NewCmdAuthLogin(argv...), NewCmdAuthLogout(argv...), NewCmdAuthToken(options)) + return cmd +} + +func NewCmdAuthToken(options []crane.Option) *cobra.Command { + var ( + header bool + push bool + mounts []string + ) + cmd := &cobra.Command{ + Use: "token REPO", + Short: "Retrieves a token for a remote repo", + Example: `# If you wanted to mount a blob from debian to ubuntu. +$ curl -H "$(crane auth token -H --push --mount debian ubuntu)" ... + +# To get the raw list tags response +$ curl -H "$(crane auth token -H ubuntu)" https://index.docker.io/v2/library/ubuntu/tags/list +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + repo, err := name.NewRepository(args[0]) + if err != nil { + return err + } + o := crane.GetOptions(options...) + + t := transport.NewLogger(o.Transport) + pr, err := transport.Ping(cmd.Context(), repo.Registry, t) + if err != nil { + return err + } + + auth, err := o.Keychain.Resolve(repo) + if err != nil { + return err + } + + scopes := []string{repo.Scope(transport.PullScope)} + if push { + scopes[0] = repo.Scope(transport.PushScope) + } + + for _, m := range mounts { + mr, err := name.NewRepository(m) + if err != nil { + return err + } + scopes = append(scopes, mr.Scope(transport.PullScope)) + } + + tr, err := transport.Exchange(cmd.Context(), repo.Registry, auth, t, scopes, pr) + if err != nil { + return err + } + + if header { + fmt.Printf("Authorization: Bearer %s", tr.Token) + return nil + } + + if err := json.NewEncoder(os.Stdout).Encode(tr); err != nil { + return err + } + + return nil + }, + } + cmd.Flags().StringSliceVarP(&mounts, "mount", "m", []string{}, "Scopes to mount from") + cmd.Flags().BoolVarP(&header, "header", "H", false, "Output in header format") + cmd.Flags().BoolVar(&push, "push", false, "Request push scopes") return cmd } diff --git a/cmd/crane/doc/crane_auth.md b/cmd/crane/doc/crane_auth.md index 8daa18a78..b1817ee59 100644 --- a/cmd/crane/doc/crane_auth.md +++ b/cmd/crane/doc/crane_auth.md @@ -27,4 +27,5 @@ crane auth [flags] * [crane auth get](crane_auth_get.md) - Implements a credential helper * [crane auth login](crane_auth_login.md) - Log in to a registry * [crane auth logout](crane_auth_logout.md) - Log out of a registry +* [crane auth token](crane_auth_token.md) - Retrieves a token for a remote repo diff --git a/cmd/crane/doc/crane_auth_token.md b/cmd/crane/doc/crane_auth_token.md new file mode 100644 index 000000000..190664065 --- /dev/null +++ b/cmd/crane/doc/crane_auth_token.md @@ -0,0 +1,41 @@ +## crane auth token + +Retrieves a token for a remote repo + +``` +crane auth token REPO [flags] +``` + +### Examples + +``` +# If you wanted to mount a blob from debian to ubuntu. +$ curl -H "$(crane auth token -H --push --mount debian ubuntu)" ... + +# To get the raw list tags response +$ curl -H "$(crane auth token -H ubuntu)" https://index.docker.io/v2/library/ubuntu/tags/list + +``` + +### Options + +``` + -H, --header Output in header format + -h, --help help for token + -m, --mount strings Scopes to mount from + --push Request push scopes +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane auth](crane_auth.md) - Log in or access credentials + diff --git a/pkg/crane/options.go b/pkg/crane/options.go index e3b7e238f..d9d441761 100644 --- a/pkg/crane/options.go +++ b/pkg/crane/options.go @@ -27,13 +27,13 @@ import ( // Options hold the options that crane uses when calling other packages. type Options struct { - Name []name.Option - Remote []remote.Option - Platform *v1.Platform - Keychain authn.Keychain + Name []name.Option + Remote []remote.Option + Platform *v1.Platform + Keychain authn.Keychain + Transport http.RoundTripper auth authn.Authenticator - transport http.RoundTripper insecure bool jobs int noclobber bool @@ -64,13 +64,15 @@ func makeOptions(opts ...Option) Options { // Allow for untrusted certificates if the user // passed Insecure but no custom transport. - if opt.insecure && opt.transport == nil { + if opt.insecure && opt.Transport == nil { transport := remote.DefaultTransport.(*http.Transport).Clone() transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, //nolint: gosec } WithTransport(transport)(&opt) + } else if opt.Transport == nil { + opt.Transport = remote.DefaultTransport } return opt @@ -85,7 +87,7 @@ type Option func(*Options) func WithTransport(t http.RoundTripper) Option { return func(o *Options) { o.Remote = append(o.Remote, remote.WithTransport(t)) - o.transport = t + o.Transport = t } } diff --git a/pkg/crane/options_test.go b/pkg/crane/options_test.go index 98d73964c..eaee3cf24 100644 --- a/pkg/crane/options_test.go +++ b/pkg/crane/options_test.go @@ -34,7 +34,7 @@ func TestInsecureOptionTracking(t *testing.T) { func TestTransportSetting(t *testing.T) { opts := GetOptions(WithTransport(remote.DefaultTransport)) - if opts.transport == nil { + if opts.Transport == nil { t.Error("expected crane transport to be set when user passes WithTransport") } } @@ -44,7 +44,7 @@ func TestInsecureTransport(t *testing.T) { opts := GetOptions(Insecure) var transport *http.Transport var ok bool - if transport, ok = opts.transport.(*http.Transport); !ok { + if transport, ok = opts.Transport.(*http.Transport); !ok { t.Fatal("Unable to successfully assert default transport") } diff --git a/pkg/v1/remote/transport/bearer.go b/pkg/v1/remote/transport/bearer.go index ea07ff6ab..1af96d182 100644 --- a/pkg/v1/remote/transport/bearer.go +++ b/pkg/v1/remote/transport/bearer.go @@ -32,6 +32,70 @@ import ( "github.com/google/go-containerregistry/pkg/name" ) +type Token struct { + Token string `json:"token"` + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` +} + +// Exchange requests a registry Token with the given scopes. +func Exchange(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string, pr *Challenge) (*Token, error) { + if strings.ToLower(pr.Scheme) != "bearer" { + // TODO: Pretend token for basic? + return nil, fmt.Errorf("challenge scheme %q is not bearer", pr.Scheme) + } + bt, err := fromChallenge(reg, auth, t, pr) + if err != nil { + return nil, err + } + authcfg, err := auth.Authorization() + if err != nil { + return nil, err + } + tok, err := bt.Refresh(ctx, authcfg) + if err != nil { + return nil, err + } + return tok, nil +} + +// FromToken returns a transport given a Challenge + Token. +func FromToken(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, tok *Token) (http.RoundTripper, error) { + if strings.ToLower(pr.Scheme) != "bearer" { + return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil + } + bt, err := fromChallenge(reg, auth, t, pr) + if err != nil { + return nil, err + } + if tok.Token != "" { + bt.bearer.RegistryToken = tok.Token + } + return &Wrapper{bt}, nil +} + +func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge) (*bearerTransport, error) { + // We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth. + realm, ok := pr.Parameters["realm"] + if !ok { + return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters) + } + service := pr.Parameters["service"] + scheme := "https" + if pr.Insecure { + scheme = "http" + } + return &bearerTransport{ + inner: t, + basic: auth, + realm: realm, + registry: reg, + service: service, + scheme: scheme, + }, nil +} + type bearerTransport struct { // Wrapped by bearerTransport. inner http.RoundTripper @@ -73,7 +137,7 @@ func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) { // we are redirected, only set it when the authorization header matches // the registry with which we are interacting. // In case of redirect http.Client can use an empty Host, check URL too. - if matchesHost(bt.registry, in, bt.scheme) { + if matchesHost(bt.registry.RegistryStr(), in, bt.scheme) { hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken) in.Header.Set("Authorization", hdr) } @@ -135,7 +199,36 @@ func (bt *bearerTransport) refresh(ctx context.Context) error { return nil } - var content []byte + response, err := bt.Refresh(ctx, auth) + if err != nil { + return err + } + + // Some registries set access_token instead of token. See #54. + if response.AccessToken != "" { + response.Token = response.AccessToken + } + + // Find a token to turn into a Bearer authenticator + if response.Token != "" { + bt.bearer.RegistryToken = response.Token + } + + // If we obtained a refresh token from the oauth flow, use that for refresh() now. + if response.RefreshToken != "" { + bt.basic = authn.FromConfig(authn.AuthConfig{ + IdentityToken: response.RefreshToken, + }) + } + + return nil +} + +func (bt *bearerTransport) Refresh(ctx context.Context, auth *authn.AuthConfig) (*Token, error) { + var ( + content []byte + err error + ) if auth.IdentityToken != "" { // If the secret being stored is an identity token, // the Username should be set to , which indicates @@ -152,48 +245,25 @@ func (bt *bearerTransport) refresh(ctx context.Context) error { content, err = bt.refreshBasic(ctx) } if err != nil { - return err - } - - // Some registries don't have "token" in the response. See #54. - type tokenResponse struct { - Token string `json:"token"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - // TODO: handle expiry? + return nil, err } - var response tokenResponse + var response Token if err := json.Unmarshal(content, &response); err != nil { - return err - } - - // Some registries set access_token instead of token. - if response.AccessToken != "" { - response.Token = response.AccessToken - } - - // Find a token to turn into a Bearer authenticator - if response.Token != "" { - bt.bearer.RegistryToken = response.Token - } else { - return fmt.Errorf("no token in bearer response:\n%s", content) + return nil, err } - // If we obtained a refresh token from the oauth flow, use that for refresh() now. - if response.RefreshToken != "" { - bt.basic = authn.FromConfig(authn.AuthConfig{ - IdentityToken: response.RefreshToken, - }) + if response.Token == "" && response.AccessToken == "" { + return &response, fmt.Errorf("no token in bearer response:\n%s", content) } - return nil + return &response, nil } -func matchesHost(reg name.Registry, in *http.Request, scheme string) bool { +func matchesHost(host string, in *http.Request, scheme string) bool { canonicalHeaderHost := canonicalAddress(in.Host, scheme) canonicalURLHost := canonicalAddress(in.URL.Host, scheme) - canonicalRegistryHost := canonicalAddress(reg.RegistryStr(), scheme) + canonicalRegistryHost := canonicalAddress(host, scheme) return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost } diff --git a/pkg/v1/remote/transport/ping.go b/pkg/v1/remote/transport/ping.go index d852ef845..799c7ea08 100644 --- a/pkg/v1/remote/transport/ping.go +++ b/pkg/v1/remote/transport/ping.go @@ -28,33 +28,22 @@ import ( "github.com/google/go-containerregistry/pkg/name" ) -type challenge string - -const ( - anonymous challenge = "anonymous" - basic challenge = "basic" - bearer challenge = "bearer" -) - // 300ms is the default fallback period for go's DNS dialer but we could make this configurable. var fallbackDelay = 300 * time.Millisecond -type pingResp struct { - challenge challenge +type Challenge struct { + Scheme string // Following the challenge there are often key/value pairs // e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz" - parameters map[string]string + Parameters map[string]string - // The registry's scheme to use. Communicates whether we fell back to http. - scheme string + // Whether we had to use http to complete the Ping. + Insecure bool } -func (c challenge) Canonical() challenge { - return challenge(strings.ToLower(string(c))) -} - -func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingResp, error) { +// Ping does a GET /v2/ against the registry and returns the response. +func Ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*Challenge, error) { // This first attempts to use "https" for every request, falling back to http // if the registry matches our localhost heuristic or if it is intentionally // set to insecure via name.NewInsecureRegistry. @@ -68,9 +57,9 @@ func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingRes return pingParallel(ctx, reg, t, schemes) } -func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*pingResp, error) { +func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*Challenge, error) { client := http.Client{Transport: t} - url := fmt.Sprintf("%s://%s/v2/", scheme, reg.Name()) + url := fmt.Sprintf("%s://%s/v2/", scheme, reg.RegistryStr()) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err @@ -86,27 +75,28 @@ func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, sch resp.Body.Close() }() + insecure := scheme == "http" + switch resp.StatusCode { case http.StatusOK: // If we get a 200, then no authentication is needed. - return &pingResp{ - challenge: anonymous, - scheme: scheme, + return &Challenge{ + Insecure: insecure, }, nil case http.StatusUnauthorized: if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 { // If we hit more than one, let's try to find one that we know how to handle. wac := pickFromMultipleChallenges(challenges) - return &pingResp{ - challenge: challenge(wac.Scheme).Canonical(), - parameters: wac.Parameters, - scheme: scheme, + return &Challenge{ + Scheme: wac.Scheme, + Parameters: wac.Parameters, + Insecure: insecure, }, nil } // Otherwise, just return the challenge without parameters. - return &pingResp{ - challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(), - scheme: scheme, + return &Challenge{ + Scheme: resp.Header.Get("WWW-Authenticate"), + Insecure: insecure, }, nil default: return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized) @@ -114,12 +104,12 @@ func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, sch } // Based on the golang happy eyeballs dialParallel impl in net/dial.go. -func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*pingResp, error) { +func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*Challenge, error) { returned := make(chan struct{}) defer close(returned) type pingResult struct { - *pingResp + *Challenge error primary bool done bool @@ -130,7 +120,7 @@ func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, s startRacer := func(ctx context.Context, scheme string) { pr, err := pingSingle(ctx, reg, t, scheme) select { - case results <- pingResult{pingResp: pr, error: err, primary: scheme == "https", done: true}: + case results <- pingResult{Challenge: pr, error: err, primary: scheme == "https", done: true}: case <-returned: if pr != nil { logs.Debug.Printf("%s lost race", scheme) @@ -156,7 +146,7 @@ func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, s case res := <-results: if res.error == nil { - return res.pingResp, nil + return res.Challenge, nil } if res.primary { primary = res @@ -164,7 +154,7 @@ func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, s fallback = res } if primary.done && fallback.done { - return nil, multierrs([]error{primary.error, fallback.error}) + return nil, multierrs{primary.error, fallback.error} } if res.primary && fallbackTimer.Stop() { // Primary failed and we haven't started the fallback, diff --git a/pkg/v1/remote/transport/ping_test.go b/pkg/v1/remote/transport/ping_test.go index c2ad119bd..36d79ab20 100644 --- a/pkg/v1/remote/transport/ping_test.go +++ b/pkg/v1/remote/transport/ping_test.go @@ -43,15 +43,15 @@ func TestPingNoChallenge(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err != nil { t.Errorf("ping() = %v", err) } - if pr.challenge != anonymous { - t.Errorf("ping(); got %v, want %v", pr.challenge, anonymous) + if pr.Scheme != "" { + t.Errorf("ping(); got %v, want %v", pr.Scheme, "") } - if pr.scheme != "http" { - t.Errorf("ping(); got %v, want %v", pr.scheme, "http") + if !pr.Insecure { + t.Errorf("ping(); got %v, want %v", pr.Insecure, true) } } @@ -68,14 +68,14 @@ func TestPingBasicChallengeNoParams(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err != nil { t.Errorf("ping() = %v", err) } - if pr.challenge != basic { - t.Errorf("ping(); got %v, want %v", pr.challenge, basic) + if pr.Scheme != "basic" { + t.Errorf("ping(); got %v, want %v", pr.Scheme, "basic") } - if got, want := len(pr.parameters), 0; got != want { + if got, want := len(pr.Parameters), 0; got != want { t.Errorf("ping(); got %v, want %v", got, want) } } @@ -93,14 +93,14 @@ func TestPingBearerChallengeWithParams(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err != nil { t.Errorf("ping() = %v", err) } - if pr.challenge != bearer { - t.Errorf("ping(); got %v, want %v", pr.challenge, bearer) + if pr.Scheme != "bearer" { + t.Errorf("ping(); got %v, want %v", pr.Scheme, "bearer") } - if got, want := len(pr.parameters), 1; got != want { + if got, want := len(pr.Parameters), 1; got != want { t.Errorf("ping(); got %v, want %v", got, want) } } @@ -119,14 +119,14 @@ func TestPingMultipleChallenges(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err != nil { t.Errorf("ping() = %v", err) } - if pr.challenge != basic { - t.Errorf("ping(); got %v, want %v", pr.challenge, basic) + if pr.Scheme != "basic" { + t.Errorf("ping(); got %v, want %v", pr.Scheme, "basic") } - if got, want := len(pr.parameters), 1; got != want { + if got, want := len(pr.Parameters), 1; got != want { t.Errorf("ping(); got %v, want %v", got, want) } } @@ -145,12 +145,12 @@ func TestPingMultipleNotSupportedChallenges(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err != nil { t.Errorf("ping() = %v", err) } - if pr.challenge != "negotiate" { - t.Errorf("ping(); got %v, want %v", pr.challenge, "negotiate") + if pr.Scheme != "negotiate" { + t.Errorf("ping(); got %v, want %v", pr.Scheme, "negotiate") } } @@ -167,7 +167,7 @@ func TestUnsupportedStatus(t *testing.T) { }, } - pr, err := ping(context.Background(), testRegistry, tprt) + pr, err := Ping(context.Background(), testRegistry, tprt) if err == nil { t.Errorf("ping() = %v", pr) } @@ -219,7 +219,7 @@ func TestPingHttpFallback(t *testing.T) { server.Close() } - _, err := ping(context.Background(), test.reg, tprt) + _, err := Ping(context.Background(), test.reg, tprt) if got, want := gotCount, test.wantCount; got != want { t.Errorf("%s: got %d requests, wanted %d", test.reg.String(), got, want) } diff --git a/pkg/v1/remote/transport/schemer.go b/pkg/v1/remote/transport/schemer.go index d70b6a850..05844db13 100644 --- a/pkg/v1/remote/transport/schemer.go +++ b/pkg/v1/remote/transport/schemer.go @@ -37,7 +37,7 @@ func (st *schemeTransport) RoundTrip(in *http.Request) (*http.Response, error) { // based on which scheme was successful. That is only valid for the // registry server and not e.g. a separate token server or blob storage, // so we should only override the scheme if the host is the registry. - if matchesHost(st.registry, in, st.scheme) { + if matchesHost(st.registry.String(), in, st.scheme) { in.URL.Scheme = st.scheme } return st.inner.RoundTrip(in) diff --git a/pkg/v1/remote/transport/transport.go b/pkg/v1/remote/transport/transport.go index 01fe1fa82..bd539b44f 100644 --- a/pkg/v1/remote/transport/transport.go +++ b/pkg/v1/remote/transport/transport.go @@ -16,8 +16,8 @@ package transport import ( "context" - "fmt" "net/http" + "strings" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -59,7 +59,7 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic // First we ping the registry to determine the parameters of the authentication handshake // (if one is even necessary). - pr, err := ping(ctx, reg, t) + pr, err := Ping(ctx, reg, t) if err != nil { return nil, err } @@ -69,39 +69,32 @@ func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authentic t = NewUserAgent(t, "") } + scheme := "https" + if pr.Insecure { + scheme = "http" + } + // Wrap t in a transport that selects the appropriate scheme based on the ping response. t = &schemeTransport{ - scheme: pr.scheme, + scheme: scheme, registry: reg, inner: t, } - switch pr.challenge.Canonical() { - case anonymous, basic: + if strings.ToLower(pr.Scheme) != "bearer" { return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil - case bearer: - // We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth. - realm, ok := pr.parameters["realm"] - if !ok { - return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.parameters) - } - service := pr.parameters["service"] - bt := &bearerTransport{ - inner: t, - basic: auth, - realm: realm, - registry: reg, - service: service, - scopes: scopes, - scheme: pr.scheme, - } - if err := bt.refresh(ctx); err != nil { - return nil, err - } - return &Wrapper{bt}, nil - default: - return nil, fmt.Errorf("unrecognized challenge: %s", pr.challenge) } + + bt, err := fromChallenge(reg, auth, t, pr) + if err != nil { + return nil, err + } + bt.scopes = scopes + + if err := bt.refresh(ctx); err != nil { + return nil, err + } + return &Wrapper{bt}, nil } // Wrapper results in *not* wrapping supplied transport with additional logic such as retries, useragent and debug logging diff --git a/pkg/v1/remote/transport/transport_test.go b/pkg/v1/remote/transport/transport_test.go index 10389b7e5..18625c081 100644 --- a/pkg/v1/remote/transport/transport_test.go +++ b/pkg/v1/remote/transport/transport_test.go @@ -231,26 +231,6 @@ func TestTransportSelectionBearerAuthError(t *testing.T) { } } -func TestTransportSelectionUnrecognizedChallenge(t *testing.T) { - server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("WWW-Authenticate", `Unrecognized`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - })) - defer server.Close() - tprt := &http.Transport{ - Proxy: func(req *http.Request) (*url.URL, error) { - return url.Parse(server.URL) - }, - } - - basic := &authn.Basic{Username: "foo", Password: "bar"} - tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) - if err == nil || !strings.Contains(err.Error(), "challenge") { - t.Errorf("NewWithContext() = %v, %v", tp, err) - } -} - func TestTransportAlwaysTriesHttps(t *testing.T) { // Use a NewTLSServer so that this speaks TLS even though it's localhost. // This ensures that we try https even for local registries.