Login update and endpoint refactor
Further differentiate the APIEndpoint used with V2 with the endpoint type which is only used for v1 registry interactions Rename Endpoint to V1Endpoint and remove version ambiguity Use distribution token handler for login Signed-off-by: Derek McGowan <derek@mcgstyle.net> Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
parent
e123ca925e
commit
065ddf0186
12 changed files with 276 additions and 596 deletions
246
docs/auth.go
246
docs/auth.go
|
@ -4,28 +4,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/registry/client/auth"
|
||||||
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
registrytypes "github.com/docker/engine-api/types/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Login tries to register/login to the registry server.
|
|
||||||
func Login(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
|
|
||||||
// Separates the v2 registry login logic from the v1 logic.
|
|
||||||
if registryEndpoint.Version == APIVersion2 {
|
|
||||||
return loginV2(authConfig, registryEndpoint, "" /* scope */)
|
|
||||||
}
|
|
||||||
return loginV1(authConfig, registryEndpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginV1 tries to register/login to the v1 registry server.
|
// loginV1 tries to register/login to the v1 registry server.
|
||||||
func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string, error) {
|
func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
|
||||||
var (
|
registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
|
||||||
err error
|
if err != nil {
|
||||||
serverAddress = authConfig.ServerAddress
|
return "", err
|
||||||
)
|
}
|
||||||
|
|
||||||
|
serverAddress := registryEndpoint.String()
|
||||||
|
|
||||||
logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
|
logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
|
||||||
|
|
||||||
|
@ -36,10 +33,16 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
|
||||||
loginAgainstOfficialIndex := serverAddress == IndexServer
|
loginAgainstOfficialIndex := serverAddress == IndexServer
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
|
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
||||||
resp, err := registryEndpoint.client.Do(req)
|
resp, err := registryEndpoint.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
// fallback when request could not be completed
|
||||||
|
return "", fallbackError{
|
||||||
|
err: err,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
@ -68,97 +71,82 @@ func loginV1(authConfig *types.AuthConfig, registryEndpoint *Endpoint) (string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginV2 tries to login to the v2 registry server. The given registry endpoint has been
|
type loginCredentialStore struct {
|
||||||
// pinged or setup with a list of authorization challenges. Each of these challenges are
|
authConfig *types.AuthConfig
|
||||||
// tried until one of them succeeds. Currently supported challenge schemes are:
|
}
|
||||||
// HTTP Basic Authorization
|
|
||||||
// Token Authorization with a separate token issuing server
|
func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
|
||||||
// NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For
|
return lcs.authConfig.Username, lcs.authConfig.Password
|
||||||
// now, users should create their account through other means like directly from a web page
|
}
|
||||||
// served by the v2 registry service provider. Whether this will be supported in the future
|
|
||||||
// is to be determined.
|
type fallbackError struct {
|
||||||
func loginV2(authConfig *types.AuthConfig, registryEndpoint *Endpoint, scope string) (string, error) {
|
|
||||||
logrus.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint)
|
|
||||||
var (
|
|
||||||
err error
|
err error
|
||||||
allErrors []error
|
}
|
||||||
)
|
|
||||||
|
|
||||||
for _, challenge := range registryEndpoint.AuthChallenges {
|
func (err fallbackError) Error() string {
|
||||||
params := make(map[string]string, len(challenge.Parameters)+1)
|
return err.err.Error()
|
||||||
for k, v := range challenge.Parameters {
|
}
|
||||||
params[k] = v
|
|
||||||
|
// loginV2 tries to login to the v2 registry server. The given registry
|
||||||
|
// endpoint will be pinged to get authorization challenges. These challenges
|
||||||
|
// will be used to authenticate against the registry to validate credentials.
|
||||||
|
func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
|
||||||
|
logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
|
||||||
|
|
||||||
|
modifiers := DockerHeaders(userAgent, nil)
|
||||||
|
authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
|
||||||
|
|
||||||
|
challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
|
||||||
|
if err != nil {
|
||||||
|
if !foundV2 {
|
||||||
|
err = fallbackError{err: err}
|
||||||
}
|
}
|
||||||
params["scope"] = scope
|
return "", err
|
||||||
logrus.Debugf("trying %q auth challenge with params %v", challenge.Scheme, params)
|
}
|
||||||
|
|
||||||
switch strings.ToLower(challenge.Scheme) {
|
creds := loginCredentialStore{
|
||||||
case "basic":
|
authConfig: authConfig,
|
||||||
err = tryV2BasicAuthLogin(authConfig, params, registryEndpoint)
|
}
|
||||||
case "bearer":
|
|
||||||
err = tryV2TokenAuthLogin(authConfig, params, registryEndpoint)
|
tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
|
||||||
default:
|
basicHandler := auth.NewBasicHandler(creds)
|
||||||
// Unsupported challenge types are explicitly skipped.
|
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||||
err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme)
|
tr := transport.NewTransport(authTransport, modifiers...)
|
||||||
|
|
||||||
|
loginClient := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
|
||||||
|
req, err := http.NewRequest("GET", endpointStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
if !foundV2 {
|
||||||
|
err = fallbackError{err: err}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := loginClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if !foundV2 {
|
||||||
|
err = fallbackError{err: err}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
|
||||||
|
err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
|
if !foundV2 {
|
||||||
|
err = fallbackError{err: err}
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return "Login Succeeded", nil
|
return "Login Succeeded", nil
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err)
|
|
||||||
|
|
||||||
allErrors = append(allErrors, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryV2BasicAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
|
|
||||||
req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
|
||||||
|
|
||||||
resp, err := registryEndpoint.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryV2TokenAuthLogin(authConfig *types.AuthConfig, params map[string]string, registryEndpoint *Endpoint) error {
|
|
||||||
token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", registryEndpoint.Path(""), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
|
|
||||||
resp, err := registryEndpoint.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
||||||
|
@ -193,3 +181,63 @@ func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registryt
|
||||||
// When all else fails, return an empty auth config
|
// When all else fails, return an empty auth config
|
||||||
return types.AuthConfig{}
|
return types.AuthConfig{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PingResponseError is used when the response from a ping
|
||||||
|
// was received but invalid.
|
||||||
|
type PingResponseError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err PingResponseError) Error() string {
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PingV2Registry attempts to ping a v2 registry and on success return a
|
||||||
|
// challenge manager for the supported authentication types and
|
||||||
|
// whether v2 was confirmed by the response. If a response is received but
|
||||||
|
// cannot be interpreted a PingResponseError will be returned.
|
||||||
|
func PingV2Registry(endpoint APIEndpoint, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
|
||||||
|
var (
|
||||||
|
foundV2 = false
|
||||||
|
v2Version = auth.APIVersion{
|
||||||
|
Type: "registry",
|
||||||
|
Version: "2.0",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pingClient := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
|
||||||
|
req, err := http.NewRequest("GET", endpointStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
resp, err := pingClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
|
||||||
|
for _, pingVersion := range versions {
|
||||||
|
if pingVersion == v2Version {
|
||||||
|
// The version header indicates we're definitely
|
||||||
|
// talking to a v2 registry. So don't allow future
|
||||||
|
// fallbacks to the v1 protocol.
|
||||||
|
|
||||||
|
foundV2 = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
challengeManager := auth.NewSimpleChallengeManager()
|
||||||
|
if err := challengeManager.AddResponse(resp); err != nil {
|
||||||
|
return nil, foundV2, PingResponseError{
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return challengeManager, foundV2, nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,150 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Octet types from RFC 2616.
|
|
||||||
type octetType byte
|
|
||||||
|
|
||||||
// AuthorizationChallenge carries information
|
|
||||||
// from a WWW-Authenticate response header.
|
|
||||||
type AuthorizationChallenge struct {
|
|
||||||
Scheme string
|
|
||||||
Parameters map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
var octetTypes [256]octetType
|
|
||||||
|
|
||||||
const (
|
|
||||||
isToken octetType = 1 << iota
|
|
||||||
isSpace
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// OCTET = <any 8-bit sequence of data>
|
|
||||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
||||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
||||||
// CR = <US-ASCII CR, carriage return (13)>
|
|
||||||
// LF = <US-ASCII LF, linefeed (10)>
|
|
||||||
// SP = <US-ASCII SP, space (32)>
|
|
||||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
|
||||||
// <"> = <US-ASCII double-quote mark (34)>
|
|
||||||
// CRLF = CR LF
|
|
||||||
// LWS = [CRLF] 1*( SP | HT )
|
|
||||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
|
||||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
|
||||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
|
||||||
// token = 1*<any CHAR except CTLs or separators>
|
|
||||||
// qdtext = <any TEXT except <">>
|
|
||||||
|
|
||||||
for c := 0; c < 256; c++ {
|
|
||||||
var t octetType
|
|
||||||
isCtl := c <= 31 || c == 127
|
|
||||||
isChar := 0 <= c && c <= 127
|
|
||||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
|
||||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
|
||||||
t |= isSpace
|
|
||||||
}
|
|
||||||
if isChar && !isCtl && !isSeparator {
|
|
||||||
t |= isToken
|
|
||||||
}
|
|
||||||
octetTypes[c] = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAuthHeader(header http.Header) []*AuthorizationChallenge {
|
|
||||||
var challenges []*AuthorizationChallenge
|
|
||||||
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
|
||||||
v, p := parseValueAndParams(h)
|
|
||||||
if v != "" {
|
|
||||||
challenges = append(challenges, &AuthorizationChallenge{Scheme: v, Parameters: p})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return challenges
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseValueAndParams(header string) (value string, params map[string]string) {
|
|
||||||
params = make(map[string]string)
|
|
||||||
value, s := expectToken(header)
|
|
||||||
if value == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
value = strings.ToLower(value)
|
|
||||||
s = "," + skipSpace(s)
|
|
||||||
for strings.HasPrefix(s, ",") {
|
|
||||||
var pkey string
|
|
||||||
pkey, s = expectToken(skipSpace(s[1:]))
|
|
||||||
if pkey == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(s, "=") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var pvalue string
|
|
||||||
pvalue, s = expectTokenOrQuoted(s[1:])
|
|
||||||
if pvalue == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pkey = strings.ToLower(pkey)
|
|
||||||
params[pkey] = pvalue
|
|
||||||
s = skipSpace(s)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipSpace(s string) (rest string) {
|
|
||||||
i := 0
|
|
||||||
for ; i < len(s); i++ {
|
|
||||||
if octetTypes[s[i]]&isSpace == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectToken(s string) (token, rest string) {
|
|
||||||
i := 0
|
|
||||||
for ; i < len(s); i++ {
|
|
||||||
if octetTypes[s[i]]&isToken == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[:i], s[i:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func expectTokenOrQuoted(s string) (value string, rest string) {
|
|
||||||
if !strings.HasPrefix(s, "\"") {
|
|
||||||
return expectToken(s)
|
|
||||||
}
|
|
||||||
s = s[1:]
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
switch s[i] {
|
|
||||||
case '"':
|
|
||||||
return s[:i], s[i+1:]
|
|
||||||
case '\\':
|
|
||||||
p := make([]byte, len(s)-1)
|
|
||||||
j := copy(p, s[:i])
|
|
||||||
escape := true
|
|
||||||
for i = i + i; i < len(s); i++ {
|
|
||||||
b := s[i]
|
|
||||||
switch {
|
|
||||||
case escape:
|
|
||||||
escape = false
|
|
||||||
p[j] = b
|
|
||||||
j++
|
|
||||||
case b == '\\':
|
|
||||||
escape = true
|
|
||||||
case b == '"':
|
|
||||||
return string(p[:j]), s[i+1:]
|
|
||||||
default:
|
|
||||||
p[j] = b
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", ""
|
|
||||||
}
|
|
|
@ -49,6 +49,9 @@ var (
|
||||||
V2Only = false
|
V2Only = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// for mocking in unit tests
|
||||||
|
var lookupIP = net.LookupIP
|
||||||
|
|
||||||
// InstallFlags adds command-line options to the top-level flag parser for
|
// InstallFlags adds command-line options to the top-level flag parser for
|
||||||
// the current process.
|
// the current process.
|
||||||
func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) {
|
func (options *Options) InstallFlags(cmd *flag.FlagSet, usageFn func(string) string) {
|
||||||
|
|
|
@ -14,12 +14,13 @@ func TestEndpointParse(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{IndexServer, IndexServer},
|
{IndexServer, IndexServer},
|
||||||
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
||||||
{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
|
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"},
|
||||||
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
|
{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
|
||||||
{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
|
{"http://0.0.0.0:5000/nonversion/", "http://0.0.0.0:5000/nonversion/v1/"},
|
||||||
|
{"http://0.0.0.0:5000/v0/", "http://0.0.0.0:5000/v0/v1/"},
|
||||||
}
|
}
|
||||||
for _, td := range testData {
|
for _, td := range testData {
|
||||||
e, err := newEndpointFromStr(td.str, nil, "", nil)
|
e, err := newV1EndpointFromStr(td.str, nil, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%q: %s", td.str, err)
|
t.Errorf("%q: %s", td.str, err)
|
||||||
}
|
}
|
||||||
|
@ -33,21 +34,26 @@ func TestEndpointParse(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEndpointParseInvalid(t *testing.T) {
|
||||||
|
testData := []string{
|
||||||
|
"http://0.0.0.0:5000/v2/",
|
||||||
|
}
|
||||||
|
for _, td := range testData {
|
||||||
|
e, err := newV1EndpointFromStr(td, nil, "", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error parsing %q: parsed as %q", td, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that a registry endpoint that responds with a 401 only is determined
|
// Ensure that a registry endpoint that responds with a 401 only is determined
|
||||||
// to be a v1 registry unless it includes a valid v2 API header.
|
// to be a valid v1 registry endpoint
|
||||||
func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
|
func TestValidateEndpoint(t *testing.T) {
|
||||||
requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
})
|
})
|
||||||
|
|
||||||
requireBasicAuthHandlerV2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// This mock server supports v2.0, v2.1, v42.0, and v100.0
|
|
||||||
w.Header().Add("Docker-Distribution-API-Version", "registry/100.0 registry/42.0")
|
|
||||||
w.Header().Add("Docker-Distribution-API-Version", "registry/2.0 registry/2.1")
|
|
||||||
requireBasicAuthHandler.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make a test server which should validate as a v1 server.
|
// Make a test server which should validate as a v1 server.
|
||||||
testServer := httptest.NewServer(requireBasicAuthHandler)
|
testServer := httptest.NewServer(requireBasicAuthHandler)
|
||||||
defer testServer.Close()
|
defer testServer.Close()
|
||||||
|
@ -57,9 +63,8 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testEndpoint := Endpoint{
|
testEndpoint := V1Endpoint{
|
||||||
URL: testServerURL,
|
URL: testServerURL,
|
||||||
Version: APIVersionUnknown,
|
|
||||||
client: HTTPClient(NewTransport(nil)),
|
client: HTTPClient(NewTransport(nil)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,27 +72,7 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if testEndpoint.Version != APIVersion1 {
|
if testEndpoint.URL.Scheme != "http" {
|
||||||
t.Fatalf("expected endpoint to validate to %d, got %d", APIVersion1, testEndpoint.Version)
|
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
||||||
}
|
|
||||||
|
|
||||||
// Make a test server which should validate as a v2 server.
|
|
||||||
testServer = httptest.NewServer(requireBasicAuthHandlerV2)
|
|
||||||
defer testServer.Close()
|
|
||||||
|
|
||||||
testServerURL, err = url.Parse(testServer.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testEndpoint.URL = testServerURL
|
|
||||||
testEndpoint.Version = APIVersionUnknown
|
|
||||||
|
|
||||||
if err = validateEndpoint(&testEndpoint); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if testEndpoint.Version != APIVersion2 {
|
|
||||||
t.Fatalf("expected endpoint to validate to %d, got %d", APIVersion2, testEndpoint.Version)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,60 +5,35 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
registrytypes "github.com/docker/engine-api/types/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// for mocking in unit tests
|
// V1Endpoint stores basic information about a V1 registry endpoint.
|
||||||
var lookupIP = net.LookupIP
|
type V1Endpoint struct {
|
||||||
|
client *http.Client
|
||||||
// scans string for api version in the URL path. returns the trimmed address, if version found, string and API version.
|
URL *url.URL
|
||||||
func scanForAPIVersion(address string) (string, APIVersion) {
|
IsSecure bool
|
||||||
var (
|
|
||||||
chunks []string
|
|
||||||
apiVersionStr string
|
|
||||||
)
|
|
||||||
|
|
||||||
if strings.HasSuffix(address, "/") {
|
|
||||||
address = address[:len(address)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks = strings.Split(address, "/")
|
|
||||||
apiVersionStr = chunks[len(chunks)-1]
|
|
||||||
|
|
||||||
for k, v := range apiVersions {
|
|
||||||
if apiVersionStr == v {
|
|
||||||
address = strings.Join(chunks[:len(chunks)-1], "/")
|
|
||||||
return address, k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return address, APIVersionUnknown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpoint parses the given address to return a registry endpoint. v can be used to
|
// NewV1Endpoint parses the given address to return a registry endpoint. v can be used to
|
||||||
// specify a specific endpoint version
|
// specify a specific endpoint version
|
||||||
func NewEndpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header, v APIVersion) (*Endpoint, error) {
|
func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
||||||
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
|
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := newEndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
|
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if v != APIVersionUnknown {
|
|
||||||
endpoint.Version = v
|
|
||||||
}
|
|
||||||
if err := validateEndpoint(endpoint); err != nil {
|
if err := validateEndpoint(endpoint); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -66,7 +41,7 @@ func NewEndpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders h
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpoint(endpoint *Endpoint) error {
|
func validateEndpoint(endpoint *V1Endpoint) error {
|
||||||
logrus.Debugf("pinging registry endpoint %s", endpoint)
|
logrus.Debugf("pinging registry endpoint %s", endpoint)
|
||||||
|
|
||||||
// Try HTTPS ping to registry
|
// Try HTTPS ping to registry
|
||||||
|
@ -93,11 +68,10 @@ func validateEndpoint(endpoint *Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEndpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*Endpoint, error) {
|
func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
||||||
endpoint := &Endpoint{
|
endpoint := &V1Endpoint{
|
||||||
IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
|
IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
|
||||||
URL: new(url.URL),
|
URL: new(url.URL),
|
||||||
Version: APIVersionUnknown,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*endpoint.URL = address
|
*endpoint.URL = address
|
||||||
|
@ -108,86 +82,69 @@ func newEndpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaH
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*Endpoint, error) {
|
// trimV1Address trims the version off the address and returns the
|
||||||
|
// trimmed address or an error if there is a non-V1 version.
|
||||||
|
func trimV1Address(address string) (string, error) {
|
||||||
|
var (
|
||||||
|
chunks []string
|
||||||
|
apiVersionStr string
|
||||||
|
)
|
||||||
|
|
||||||
|
if strings.HasSuffix(address, "/") {
|
||||||
|
address = address[:len(address)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks = strings.Split(address, "/")
|
||||||
|
apiVersionStr = chunks[len(chunks)-1]
|
||||||
|
if apiVersionStr == "v1" {
|
||||||
|
return strings.Join(chunks[:len(chunks)-1], "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range apiVersions {
|
||||||
|
if k != APIVersion1 && apiVersionStr == v {
|
||||||
|
return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
||||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
||||||
address = "https://" + address
|
address = "https://" + address
|
||||||
}
|
}
|
||||||
|
|
||||||
trimmedAddress, detectedVersion := scanForAPIVersion(address)
|
address, err := trimV1Address(address)
|
||||||
|
|
||||||
uri, err := url.Parse(trimmedAddress)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := newEndpoint(*uri, tlsConfig, userAgent, metaHeaders)
|
uri, err := url.Parse(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint.Version = detectedVersion
|
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint stores basic information about a registry endpoint.
|
|
||||||
type Endpoint struct {
|
|
||||||
client *http.Client
|
|
||||||
URL *url.URL
|
|
||||||
Version APIVersion
|
|
||||||
IsSecure bool
|
|
||||||
AuthChallenges []*AuthorizationChallenge
|
|
||||||
URLBuilder *v2.URLBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the formatted URL for the root of this registry Endpoint
|
// Get the formatted URL for the root of this registry Endpoint
|
||||||
func (e *Endpoint) String() string {
|
func (e *V1Endpoint) String() string {
|
||||||
return fmt.Sprintf("%s/v%d/", e.URL, e.Version)
|
return e.URL.String() + "/v1/"
|
||||||
}
|
|
||||||
|
|
||||||
// VersionString returns a formatted string of this
|
|
||||||
// endpoint address using the given API Version.
|
|
||||||
func (e *Endpoint) VersionString(version APIVersion) string {
|
|
||||||
return fmt.Sprintf("%s/v%d/", e.URL, version)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path returns a formatted string for the URL
|
// Path returns a formatted string for the URL
|
||||||
// of this endpoint with the given path appended.
|
// of this endpoint with the given path appended.
|
||||||
func (e *Endpoint) Path(path string) string {
|
func (e *V1Endpoint) Path(path string) string {
|
||||||
return fmt.Sprintf("%s/v%d/%s", e.URL, e.Version, path)
|
return e.URL.String() + "/v1/" + path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping pings the remote endpoint with v2 and v1 pings to determine the API
|
// Ping returns a PingResult which indicates whether the registry is standalone or not.
|
||||||
// version. It returns a PingResult containing the discovered version. The
|
func (e *V1Endpoint) Ping() (PingResult, error) {
|
||||||
// PingResult also indicates whether the registry is standalone or not.
|
|
||||||
func (e *Endpoint) Ping() (PingResult, error) {
|
|
||||||
// The ping logic to use is determined by the registry endpoint version.
|
|
||||||
switch e.Version {
|
|
||||||
case APIVersion1:
|
|
||||||
return e.pingV1()
|
|
||||||
case APIVersion2:
|
|
||||||
return e.pingV2()
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIVersionUnknown
|
|
||||||
// We should try v2 first...
|
|
||||||
e.Version = APIVersion2
|
|
||||||
regInfo, errV2 := e.pingV2()
|
|
||||||
if errV2 == nil {
|
|
||||||
return regInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... then fallback to v1.
|
|
||||||
e.Version = APIVersion1
|
|
||||||
regInfo, errV1 := e.pingV1()
|
|
||||||
if errV1 == nil {
|
|
||||||
return regInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Version = APIVersionUnknown
|
|
||||||
return PingResult{}, fmt.Errorf("unable to ping registry endpoint %s\nv2 ping attempt failed with error: %s\n v1 ping attempt failed with error: %s", e, errV2, errV1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Endpoint) pingV1() (PingResult, error) {
|
|
||||||
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
||||||
|
|
||||||
if e.String() == IndexServer {
|
if e.String() == IndexServer {
|
||||||
|
@ -240,51 +197,3 @@ func (e *Endpoint) pingV1() (PingResult, error) {
|
||||||
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
|
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Endpoint) pingV2() (PingResult, error) {
|
|
||||||
logrus.Debugf("attempting v2 ping for registry endpoint %s", e)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", e.Path(""), nil)
|
|
||||||
if err != nil {
|
|
||||||
return PingResult{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := e.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return PingResult{}, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// The endpoint may have multiple supported versions.
|
|
||||||
// Ensure it supports the v2 Registry API.
|
|
||||||
var supportsV2 bool
|
|
||||||
|
|
||||||
HeaderLoop:
|
|
||||||
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
|
|
||||||
for _, versionName := range strings.Fields(supportedVersions) {
|
|
||||||
if versionName == "registry/2.0" {
|
|
||||||
supportsV2 = true
|
|
||||||
break HeaderLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !supportsV2 {
|
|
||||||
return PingResult{}, fmt.Errorf("%s does not appear to be a v2 registry endpoint", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
// It would seem that no authentication/authorization is required.
|
|
||||||
// So we don't need to parse/add any authorization schemes.
|
|
||||||
return PingResult{Standalone: true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusUnauthorized {
|
|
||||||
// Parse the WWW-Authenticate Header and store the challenges
|
|
||||||
// on this endpoint object.
|
|
||||||
e.AuthChallenges = parseAuthHeader(resp.Header)
|
|
||||||
return PingResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return PingResult{}, fmt.Errorf("v2 registry endpoint returned status %d: %q", resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ const (
|
||||||
|
|
||||||
func spawnTestRegistrySession(t *testing.T) *Session {
|
func spawnTestRegistrySession(t *testing.T) *Session {
|
||||||
authConfig := &types.AuthConfig{}
|
authConfig := &types.AuthConfig{}
|
||||||
endpoint, err := NewEndpoint(makeIndex("/v1/"), "", nil, APIVersionUnknown)
|
endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
|
||||||
|
|
||||||
func TestPingRegistryEndpoint(t *testing.T) {
|
func TestPingRegistryEndpoint(t *testing.T) {
|
||||||
testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
|
testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
|
||||||
ep, err := NewEndpoint(index, "", nil, APIVersionUnknown)
|
ep, err := NewV1Endpoint(index, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,8 @@ func TestPingRegistryEndpoint(t *testing.T) {
|
||||||
|
|
||||||
func TestEndpoint(t *testing.T) {
|
func TestEndpoint(t *testing.T) {
|
||||||
// Simple wrapper to fail test if err != nil
|
// Simple wrapper to fail test if err != nil
|
||||||
expandEndpoint := func(index *registrytypes.IndexInfo) *Endpoint {
|
expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint {
|
||||||
endpoint, err := NewEndpoint(index, "", nil, APIVersionUnknown)
|
endpoint, err := NewV1Endpoint(index, "", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ func TestEndpoint(t *testing.T) {
|
||||||
|
|
||||||
assertInsecureIndex := func(index *registrytypes.IndexInfo) {
|
assertInsecureIndex := func(index *registrytypes.IndexInfo) {
|
||||||
index.Secure = true
|
index.Secure = true
|
||||||
_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
|
_, err := NewV1Endpoint(index, "", nil)
|
||||||
assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
|
assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
|
||||||
assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index")
|
assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index")
|
||||||
index.Secure = false
|
index.Secure = false
|
||||||
|
@ -90,7 +90,7 @@ func TestEndpoint(t *testing.T) {
|
||||||
|
|
||||||
assertSecureIndex := func(index *registrytypes.IndexInfo) {
|
assertSecureIndex := func(index *registrytypes.IndexInfo) {
|
||||||
index.Secure = true
|
index.Secure = true
|
||||||
_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
|
_, err := NewV1Endpoint(index, "", nil)
|
||||||
assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
|
assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
|
||||||
assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
|
assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
|
||||||
index.Secure = false
|
index.Secure = false
|
||||||
|
@ -100,51 +100,33 @@ func TestEndpoint(t *testing.T) {
|
||||||
index.Name = makeURL("/v1/")
|
index.Name = makeURL("/v1/")
|
||||||
endpoint := expandEndpoint(index)
|
endpoint := expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertInsecureIndex(index)
|
assertInsecureIndex(index)
|
||||||
|
|
||||||
index.Name = makeURL("")
|
index.Name = makeURL("")
|
||||||
endpoint = expandEndpoint(index)
|
endpoint = expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertInsecureIndex(index)
|
assertInsecureIndex(index)
|
||||||
|
|
||||||
httpURL := makeURL("")
|
httpURL := makeURL("")
|
||||||
index.Name = strings.SplitN(httpURL, "://", 2)[1]
|
index.Name = strings.SplitN(httpURL, "://", 2)[1]
|
||||||
endpoint = expandEndpoint(index)
|
endpoint = expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
|
assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertInsecureIndex(index)
|
assertInsecureIndex(index)
|
||||||
|
|
||||||
index.Name = makeHTTPSURL("/v1/")
|
index.Name = makeHTTPSURL("/v1/")
|
||||||
endpoint = expandEndpoint(index)
|
endpoint = expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertSecureIndex(index)
|
assertSecureIndex(index)
|
||||||
|
|
||||||
index.Name = makeHTTPSURL("")
|
index.Name = makeHTTPSURL("")
|
||||||
endpoint = expandEndpoint(index)
|
endpoint = expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertSecureIndex(index)
|
assertSecureIndex(index)
|
||||||
|
|
||||||
httpsURL := makeHTTPSURL("")
|
httpsURL := makeHTTPSURL("")
|
||||||
index.Name = strings.SplitN(httpsURL, "://", 2)[1]
|
index.Name = strings.SplitN(httpsURL, "://", 2)[1]
|
||||||
endpoint = expandEndpoint(index)
|
endpoint = expandEndpoint(index)
|
||||||
assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
|
assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
|
||||||
if endpoint.Version != APIVersion1 {
|
|
||||||
t.Fatal("Expected endpoint to be v1")
|
|
||||||
}
|
|
||||||
assertSecureIndex(index)
|
assertSecureIndex(index)
|
||||||
|
|
||||||
badEndpoints := []string{
|
badEndpoints := []string{
|
||||||
|
@ -156,7 +138,7 @@ func TestEndpoint(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, address := range badEndpoints {
|
for _, address := range badEndpoints {
|
||||||
index.Name = address
|
index.Name = address
|
||||||
_, err := NewEndpoint(index, "", nil, APIVersionUnknown)
|
_, err := NewV1Endpoint(index, "", nil)
|
||||||
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
|
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -685,7 +667,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
pushAPIEndpoints, err := s.LookupPushEndpoints(imageName)
|
pushAPIEndpoints, err := s.LookupPushEndpoints(imageName.Hostname())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -693,7 +675,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
|
||||||
t.Fatal("Push endpoint should not contain mirror")
|
t.Fatal("Push endpoint should not contain mirror")
|
||||||
}
|
}
|
||||||
|
|
||||||
pullAPIEndpoints, err := s.LookupPullEndpoints(imageName)
|
pullAPIEndpoints, err := s.LookupPullEndpoints(imageName.Hostname())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/docker/reference"
|
"github.com/docker/docker/reference"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
registrytypes "github.com/docker/engine-api/types/registry"
|
||||||
|
@ -28,29 +29,31 @@ func NewService(options *Options) *Service {
|
||||||
// Auth contacts the public registry with the provided credentials,
|
// Auth contacts the public registry with the provided credentials,
|
||||||
// and returns OK if authentication was successful.
|
// and returns OK if authentication was successful.
|
||||||
// It can be used to verify the validity of a client's credentials.
|
// It can be used to verify the validity of a client's credentials.
|
||||||
func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (string, error) {
|
func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) {
|
||||||
addr := authConfig.ServerAddress
|
endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress)
|
||||||
if addr == "" {
|
|
||||||
// Use the official registry address if not specified.
|
|
||||||
addr = IndexServer
|
|
||||||
}
|
|
||||||
index, err := s.ResolveIndex(addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointVersion := APIVersion(APIVersionUnknown)
|
for _, endpoint := range endpoints {
|
||||||
if V2Only {
|
login := loginV2
|
||||||
// Override the endpoint to only attempt a v2 ping
|
if endpoint.Version == APIVersion1 {
|
||||||
endpointVersion = APIVersion2
|
login = loginV1
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := NewEndpoint(index, userAgent, nil, endpointVersion)
|
status, err = login(authConfig, endpoint, userAgent)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fErr, ok := err.(fallbackError); ok {
|
||||||
|
err = fErr.err
|
||||||
|
logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
authConfig.ServerAddress = endpoint.String()
|
|
||||||
return Login(authConfig, endpoint)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitReposSearchTerm breaks a search term into an index name and remote name
|
// splitReposSearchTerm breaks a search term into an index name and remote name
|
||||||
|
@ -85,7 +88,7 @@ func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent st
|
||||||
}
|
}
|
||||||
|
|
||||||
// *TODO: Search multiple indexes.
|
// *TODO: Search multiple indexes.
|
||||||
endpoint, err := NewEndpoint(index, userAgent, http.Header(headers), APIVersionUnknown)
|
endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -129,8 +132,8 @@ type APIEndpoint struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
|
// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
|
||||||
func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*Endpoint, error) {
|
func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
||||||
return newEndpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
|
return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfig constructs a client TLS configuration based on server defaults
|
// TLSConfig constructs a client TLS configuration based on server defaults
|
||||||
|
@ -145,15 +148,15 @@ func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
|
||||||
// LookupPullEndpoints creates an list of endpoints to try to pull from, in order of preference.
|
// LookupPullEndpoints creates an list of endpoints to try to pull from, in order of preference.
|
||||||
// It gives preference to v2 endpoints over v1, mirrors over the actual
|
// It gives preference to v2 endpoints over v1, mirrors over the actual
|
||||||
// registry, and HTTPS over plain HTTP.
|
// registry, and HTTPS over plain HTTP.
|
||||||
func (s *Service) LookupPullEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
|
func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||||
return s.lookupEndpoints(repoName)
|
return s.lookupEndpoints(hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupPushEndpoints creates an list of endpoints to try to push to, in order of preference.
|
// LookupPushEndpoints creates an list of endpoints to try to push to, in order of preference.
|
||||||
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
|
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
|
||||||
// Mirrors are not included.
|
// Mirrors are not included.
|
||||||
func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
|
func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||||
allEndpoints, err := s.lookupEndpoints(repoName)
|
allEndpoints, err := s.lookupEndpoints(hostname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, endpoint := range allEndpoints {
|
for _, endpoint := range allEndpoints {
|
||||||
if !endpoint.Mirror {
|
if !endpoint.Mirror {
|
||||||
|
@ -164,8 +167,8 @@ func (s *Service) LookupPushEndpoints(repoName reference.Named) (endpoints []API
|
||||||
return endpoints, err
|
return endpoints, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
|
func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||||
endpoints, err = s.lookupV2Endpoints(repoName)
|
endpoints, err = s.lookupV2Endpoints(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -174,7 +177,7 @@ func (s *Service) lookupEndpoints(repoName reference.Named) (endpoints []APIEndp
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
legacyEndpoints, err := s.lookupV1Endpoints(repoName)
|
legacyEndpoints, err := s.lookupV1Endpoints(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
|
func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||||
var cfg = tlsconfig.ServerDefault
|
var cfg = tlsconfig.ServerDefault
|
||||||
tlsConfig := &cfg
|
tlsConfig := &cfg
|
||||||
nameString := repoName.FullName()
|
if hostname == DefaultNamespace {
|
||||||
if strings.HasPrefix(nameString, DefaultNamespace+"/") {
|
|
||||||
endpoints = append(endpoints, APIEndpoint{
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
URL: DefaultV1Registry,
|
URL: DefaultV1Registry,
|
||||||
Version: APIVersion1,
|
Version: APIVersion1,
|
||||||
|
@ -24,12 +20,6 @@ func (s *Service) lookupV1Endpoints(repoName reference.Named) (endpoints []APIEn
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
slashIndex := strings.IndexRune(nameString, '/')
|
|
||||||
if slashIndex <= 0 {
|
|
||||||
return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString)
|
|
||||||
}
|
|
||||||
hostname := nameString[:slashIndex]
|
|
||||||
|
|
||||||
tlsConfig, err = s.TLSConfig(hostname)
|
tlsConfig, err = s.TLSConfig(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
"github.com/docker/go-connections/tlsconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEndpoint, err error) {
|
func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||||
var cfg = tlsconfig.ServerDefault
|
var cfg = tlsconfig.ServerDefault
|
||||||
tlsConfig := &cfg
|
tlsConfig := &cfg
|
||||||
nameString := repoName.FullName()
|
if hostname == DefaultNamespace {
|
||||||
if strings.HasPrefix(nameString, DefaultNamespace+"/") {
|
|
||||||
// v2 mirrors
|
// v2 mirrors
|
||||||
for _, mirror := range s.Config.Mirrors {
|
for _, mirror := range s.Config.Mirrors {
|
||||||
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
||||||
|
@ -48,12 +45,6 @@ func (s *Service) lookupV2Endpoints(repoName reference.Named) (endpoints []APIEn
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
slashIndex := strings.IndexRune(nameString, '/')
|
|
||||||
if slashIndex <= 0 {
|
|
||||||
return nil, fmt.Errorf("invalid repo name: missing '/': %s", nameString)
|
|
||||||
}
|
|
||||||
hostname := nameString[:slashIndex]
|
|
||||||
|
|
||||||
tlsConfig, err = s.TLSConfig(hostname)
|
tlsConfig, err = s.TLSConfig(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -37,7 +37,7 @@ var (
|
||||||
|
|
||||||
// A Session is used to communicate with a V1 registry
|
// A Session is used to communicate with a V1 registry
|
||||||
type Session struct {
|
type Session struct {
|
||||||
indexEndpoint *Endpoint
|
indexEndpoint *V1Endpoint
|
||||||
client *http.Client
|
client *http.Client
|
||||||
// TODO(tiborvass): remove authConfig
|
// TODO(tiborvass): remove authConfig
|
||||||
authConfig *types.AuthConfig
|
authConfig *types.AuthConfig
|
||||||
|
@ -163,7 +163,7 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
|
||||||
|
|
||||||
// NewSession creates a new session
|
// NewSession creates a new session
|
||||||
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
|
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
|
||||||
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *Endpoint) (r *Session, err error) {
|
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (r *Session, err error) {
|
||||||
r = &Session{
|
r = &Session{
|
||||||
authConfig: authConfig,
|
authConfig: authConfig,
|
||||||
client: client,
|
client: client,
|
||||||
|
@ -175,7 +175,7 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *End
|
||||||
|
|
||||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
||||||
// alongside all our requests.
|
// alongside all our requests.
|
||||||
if endpoint.VersionString(1) != IndexServer && endpoint.URL.Scheme == "https" {
|
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
||||||
info, err := endpoint.Ping()
|
info, err := endpoint.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -405,7 +405,7 @@ func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
|
||||||
|
|
||||||
// GetRepositoryData returns lists of images and endpoints for the repository
|
// GetRepositoryData returns lists of images and endpoints for the repository
|
||||||
func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) {
|
func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) {
|
||||||
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.VersionString(1), name.RemoteName())
|
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName())
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling GET %s", repositoryTarget)
|
logrus.Debugf("[registry] Calling GET %s", repositoryTarget)
|
||||||
|
|
||||||
|
@ -444,7 +444,7 @@ func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, erro
|
||||||
|
|
||||||
var endpoints []string
|
var endpoints []string
|
||||||
if res.Header.Get("X-Docker-Endpoints") != "" {
|
if res.Header.Get("X-Docker-Endpoints") != "" {
|
||||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
|
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -634,7 +634,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
|
||||||
if validate {
|
if validate {
|
||||||
suffix = "images"
|
suffix = "images"
|
||||||
}
|
}
|
||||||
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.VersionString(1), remote.RemoteName(), suffix)
|
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix)
|
||||||
logrus.Debugf("[registry] PUT %s", u)
|
logrus.Debugf("[registry] PUT %s", u)
|
||||||
logrus.Debugf("Image list pushed to index:\n%s", imgListJSON)
|
logrus.Debugf("Image list pushed to index:\n%s", imgListJSON)
|
||||||
headers := map[string][]string{
|
headers := map[string][]string{
|
||||||
|
@ -680,7 +680,7 @@ func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData,
|
||||||
if res.Header.Get("X-Docker-Endpoints") == "" {
|
if res.Header.Get("X-Docker-Endpoints") == "" {
|
||||||
return nil, fmt.Errorf("Index response didn't contain any endpoints")
|
return nil, fmt.Errorf("Index response didn't contain any endpoints")
|
||||||
}
|
}
|
||||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.VersionString(1))
|
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -722,7 +722,7 @@ func shouldRedirect(response *http.Response) bool {
|
||||||
// SearchRepositories performs a search against the remote repository
|
// SearchRepositories performs a search against the remote repository
|
||||||
func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) {
|
func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) {
|
||||||
logrus.Debugf("Index server: %s", r.indexEndpoint)
|
logrus.Debugf("Index server: %s", r.indexEndpoint)
|
||||||
u := r.indexEndpoint.VersionString(1) + "search?q=" + url.QueryEscape(term)
|
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tokenResponse struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func getToken(username, password string, params map[string]string, registryEndpoint *Endpoint) (string, error) {
|
|
||||||
realm, ok := params["realm"]
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("no realm specified for token auth challenge")
|
|
||||||
}
|
|
||||||
|
|
||||||
realmURL, err := url.Parse(realm)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if realmURL.Scheme == "" {
|
|
||||||
if registryEndpoint.IsSecure {
|
|
||||||
realmURL.Scheme = "https"
|
|
||||||
} else {
|
|
||||||
realmURL.Scheme = "http"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", realmURL.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
reqParams := req.URL.Query()
|
|
||||||
service := params["service"]
|
|
||||||
scope := params["scope"]
|
|
||||||
|
|
||||||
if service != "" {
|
|
||||||
reqParams.Add("service", service)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, scopeField := range strings.Fields(scope) {
|
|
||||||
reqParams.Add("scope", scopeField)
|
|
||||||
}
|
|
||||||
|
|
||||||
if username != "" {
|
|
||||||
reqParams.Add("account", username)
|
|
||||||
req.SetBasicAuth(username, password)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.URL.RawQuery = reqParams.Encode()
|
|
||||||
|
|
||||||
resp, err := registryEndpoint.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("token auth attempt for registry %s: %s request failed with status: %d %s", registryEndpoint, req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(resp.Body)
|
|
||||||
|
|
||||||
tr := new(tokenResponse)
|
|
||||||
if err = decoder.Decode(tr); err != nil {
|
|
||||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tr.Token == "" {
|
|
||||||
return "", errors.New("authorization server did not include a token in the response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return tr.Token, nil
|
|
||||||
}
|
|
|
@ -46,18 +46,18 @@ func (av APIVersion) String() string {
|
||||||
return apiVersions[av]
|
return apiVersions[av]
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiVersions = map[APIVersion]string{
|
|
||||||
1: "v1",
|
|
||||||
2: "v2",
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Version identifiers.
|
// API Version identifiers.
|
||||||
const (
|
const (
|
||||||
APIVersionUnknown = iota
|
_ = iota
|
||||||
APIVersion1
|
APIVersion1 APIVersion = iota
|
||||||
APIVersion2
|
APIVersion2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var apiVersions = map[APIVersion]string{
|
||||||
|
APIVersion1: "v1",
|
||||||
|
APIVersion2: "v2",
|
||||||
|
}
|
||||||
|
|
||||||
// RepositoryInfo describes a repository
|
// RepositoryInfo describes a repository
|
||||||
type RepositoryInfo struct {
|
type RepositoryInfo struct {
|
||||||
reference.Named
|
reference.Named
|
||||||
|
|
Loading…
Reference in a new issue