Initial import of https://github.com/docker/docker
						commit
						1f6cc853d3
					
				|  | @ -0,0 +1,300 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/registry/client/auth" | ||||
| 	"github.com/docker/distribution/registry/client/transport" | ||||
| 	"github.com/docker/engine-api/types" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// AuthClientID is used the ClientID used for the token server
 | ||||
| 	AuthClientID = "docker" | ||||
| ) | ||||
| 
 | ||||
| // loginV1 tries to register/login to the v1 registry server.
 | ||||
| func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) { | ||||
| 	registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 
 | ||||
| 	serverAddress := registryEndpoint.String() | ||||
| 
 | ||||
| 	logrus.Debugf("attempting v1 login to registry endpoint %s", serverAddress) | ||||
| 
 | ||||
| 	if serverAddress == "" { | ||||
| 		return "", "", fmt.Errorf("Server Error: Server Address not set.") | ||||
| 	} | ||||
| 
 | ||||
| 	loginAgainstOfficialIndex := serverAddress == IndexServer | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", serverAddress+"users/", nil) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	req.SetBasicAuth(authConfig.Username, authConfig.Password) | ||||
| 	resp, err := registryEndpoint.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		// fallback when request could not be completed
 | ||||
| 		return "", "", fallbackError{ | ||||
| 			err: err, | ||||
| 		} | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	if resp.StatusCode == http.StatusOK { | ||||
| 		return "Login Succeeded", "", nil | ||||
| 	} else if resp.StatusCode == http.StatusUnauthorized { | ||||
| 		if loginAgainstOfficialIndex { | ||||
| 			return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com") | ||||
| 		} | ||||
| 		return "", "", fmt.Errorf("Wrong login/password, please try again") | ||||
| 	} else if resp.StatusCode == http.StatusForbidden { | ||||
| 		if loginAgainstOfficialIndex { | ||||
| 			return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.") | ||||
| 		} | ||||
| 		// *TODO: Use registry configuration to determine what this says, if anything?
 | ||||
| 		return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) | ||||
| 	} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
 | ||||
| 		logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body) | ||||
| 		return "", "", fmt.Errorf("Internal Server Error") | ||||
| 	} | ||||
| 	return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, | ||||
| 		resp.StatusCode, resp.Header) | ||||
| } | ||||
| 
 | ||||
| type loginCredentialStore struct { | ||||
| 	authConfig *types.AuthConfig | ||||
| } | ||||
| 
 | ||||
| func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { | ||||
| 	return lcs.authConfig.Username, lcs.authConfig.Password | ||||
| } | ||||
| 
 | ||||
| func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { | ||||
| 	return lcs.authConfig.IdentityToken | ||||
| } | ||||
| 
 | ||||
| func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { | ||||
| 	lcs.authConfig.IdentityToken = token | ||||
| } | ||||
| 
 | ||||
| type staticCredentialStore struct { | ||||
| 	auth *types.AuthConfig | ||||
| } | ||||
| 
 | ||||
| // NewStaticCredentialStore returns a credential store
 | ||||
| // which always returns the same credential values.
 | ||||
| func NewStaticCredentialStore(auth *types.AuthConfig) auth.CredentialStore { | ||||
| 	return staticCredentialStore{ | ||||
| 		auth: auth, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (scs staticCredentialStore) Basic(*url.URL) (string, string) { | ||||
| 	if scs.auth == nil { | ||||
| 		return "", "" | ||||
| 	} | ||||
| 	return scs.auth.Username, scs.auth.Password | ||||
| } | ||||
| 
 | ||||
| func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { | ||||
| 	if scs.auth == nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return scs.auth.IdentityToken | ||||
| } | ||||
| 
 | ||||
| func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) { | ||||
| } | ||||
| 
 | ||||
| type fallbackError struct { | ||||
| 	err error | ||||
| } | ||||
| 
 | ||||
| func (err fallbackError) Error() string { | ||||
| 	return err.err.Error() | ||||
| } | ||||
| 
 | ||||
| // 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, string, error) { | ||||
| 	logrus.Debugf("attempting v2 login to registry endpoint %s", strings.TrimRight(endpoint.URL.String(), "/")+"/v2/") | ||||
| 
 | ||||
| 	modifiers := DockerHeaders(userAgent, nil) | ||||
| 	authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...) | ||||
| 
 | ||||
| 	credentialAuthConfig := *authConfig | ||||
| 	creds := loginCredentialStore{ | ||||
| 		authConfig: &credentialAuthConfig, | ||||
| 	} | ||||
| 
 | ||||
| 	loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 
 | ||||
| 	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 | ||||
| 	} | ||||
| 
 | ||||
| 	return "Login Succeeded", credentialAuthConfig.IdentityToken, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, bool, error) { | ||||
| 	challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport) | ||||
| 	if err != nil { | ||||
| 		if !foundV2 { | ||||
| 			err = fallbackError{err: err} | ||||
| 		} | ||||
| 		return nil, foundV2, err | ||||
| 	} | ||||
| 
 | ||||
| 	tokenHandlerOptions := auth.TokenHandlerOptions{ | ||||
| 		Transport:     authTransport, | ||||
| 		Credentials:   creds, | ||||
| 		OfflineAccess: true, | ||||
| 		ClientID:      AuthClientID, | ||||
| 		Scopes:        scopes, | ||||
| 	} | ||||
| 	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) | ||||
| 	basicHandler := auth.NewBasicHandler(creds) | ||||
| 	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) | ||||
| 	tr := transport.NewTransport(authTransport, modifiers...) | ||||
| 
 | ||||
| 	return &http.Client{ | ||||
| 		Transport: tr, | ||||
| 		Timeout:   15 * time.Second, | ||||
| 	}, foundV2, nil | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // ResolveAuthConfig matches an auth configuration to a server address or a URL
 | ||||
| func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig { | ||||
| 	configKey := GetAuthConfigKey(index) | ||||
| 	// First try the happy case
 | ||||
| 	if c, found := authConfigs[configKey]; found || index.Official { | ||||
| 		return c | ||||
| 	} | ||||
| 
 | ||||
| 	convertToHostname := func(url string) string { | ||||
| 		stripped := url | ||||
| 		if strings.HasPrefix(url, "http://") { | ||||
| 			stripped = strings.Replace(url, "http://", "", 1) | ||||
| 		} else if strings.HasPrefix(url, "https://") { | ||||
| 			stripped = strings.Replace(url, "https://", "", 1) | ||||
| 		} | ||||
| 
 | ||||
| 		nameParts := strings.SplitN(stripped, "/", 2) | ||||
| 
 | ||||
| 		return nameParts[0] | ||||
| 	} | ||||
| 
 | ||||
| 	// Maybe they have a legacy config file, we will iterate the keys converting
 | ||||
| 	// them to the new format and testing
 | ||||
| 	for registry, ac := range authConfigs { | ||||
| 		if configKey == convertToHostname(registry) { | ||||
| 			return ac | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// When all else fails, return an empty auth config
 | ||||
| 	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 *url.URL, 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.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 | ||||
| } | ||||
|  | @ -0,0 +1,120 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/engine-api/types" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| func buildAuthConfigs() map[string]types.AuthConfig { | ||||
| 	authConfigs := map[string]types.AuthConfig{} | ||||
| 
 | ||||
| 	for _, registry := range []string{"testIndex", IndexServer} { | ||||
| 		authConfigs[registry] = types.AuthConfig{ | ||||
| 			Username: "docker-user", | ||||
| 			Password: "docker-pass", | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return authConfigs | ||||
| } | ||||
| 
 | ||||
| func TestSameAuthDataPostSave(t *testing.T) { | ||||
| 	authConfigs := buildAuthConfigs() | ||||
| 	authConfig := authConfigs["testIndex"] | ||||
| 	if authConfig.Username != "docker-user" { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 	if authConfig.Password != "docker-pass" { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 	if authConfig.Auth != "" { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestResolveAuthConfigIndexServer(t *testing.T) { | ||||
| 	authConfigs := buildAuthConfigs() | ||||
| 	indexConfig := authConfigs[IndexServer] | ||||
| 
 | ||||
| 	officialIndex := ®istrytypes.IndexInfo{ | ||||
| 		Official: true, | ||||
| 	} | ||||
| 	privateIndex := ®istrytypes.IndexInfo{ | ||||
| 		Official: false, | ||||
| 	} | ||||
| 
 | ||||
| 	resolved := ResolveAuthConfig(authConfigs, officialIndex) | ||||
| 	assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") | ||||
| 
 | ||||
| 	resolved = ResolveAuthConfig(authConfigs, privateIndex) | ||||
| 	assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServer") | ||||
| } | ||||
| 
 | ||||
| func TestResolveAuthConfigFullURL(t *testing.T) { | ||||
| 	authConfigs := buildAuthConfigs() | ||||
| 
 | ||||
| 	registryAuth := types.AuthConfig{ | ||||
| 		Username: "foo-user", | ||||
| 		Password: "foo-pass", | ||||
| 	} | ||||
| 	localAuth := types.AuthConfig{ | ||||
| 		Username: "bar-user", | ||||
| 		Password: "bar-pass", | ||||
| 	} | ||||
| 	officialAuth := types.AuthConfig{ | ||||
| 		Username: "baz-user", | ||||
| 		Password: "baz-pass", | ||||
| 	} | ||||
| 	authConfigs[IndexServer] = officialAuth | ||||
| 
 | ||||
| 	expectedAuths := map[string]types.AuthConfig{ | ||||
| 		"registry.example.com": registryAuth, | ||||
| 		"localhost:8000":       localAuth, | ||||
| 		"registry.com":         localAuth, | ||||
| 	} | ||||
| 
 | ||||
| 	validRegistries := map[string][]string{ | ||||
| 		"registry.example.com": { | ||||
| 			"https://registry.example.com/v1/", | ||||
| 			"http://registry.example.com/v1/", | ||||
| 			"registry.example.com", | ||||
| 			"registry.example.com/v1/", | ||||
| 		}, | ||||
| 		"localhost:8000": { | ||||
| 			"https://localhost:8000/v1/", | ||||
| 			"http://localhost:8000/v1/", | ||||
| 			"localhost:8000", | ||||
| 			"localhost:8000/v1/", | ||||
| 		}, | ||||
| 		"registry.com": { | ||||
| 			"https://registry.com/v1/", | ||||
| 			"http://registry.com/v1/", | ||||
| 			"registry.com", | ||||
| 			"registry.com/v1/", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for configKey, registries := range validRegistries { | ||||
| 		configured, ok := expectedAuths[configKey] | ||||
| 		if !ok { | ||||
| 			t.Fail() | ||||
| 		} | ||||
| 		index := ®istrytypes.IndexInfo{ | ||||
| 			Name: configKey, | ||||
| 		} | ||||
| 		for _, registry := range registries { | ||||
| 			authConfigs[registry] = configured | ||||
| 			resolved := ResolveAuthConfig(authConfigs, index) | ||||
| 			if resolved.Username != configured.Username || resolved.Password != configured.Password { | ||||
| 				t.Errorf("%s -> %v != %v\n", registry, resolved, configured) | ||||
| 			} | ||||
| 			delete(authConfigs, registry) | ||||
| 			resolved = ResolveAuthConfig(authConfigs, index) | ||||
| 			if resolved.Username == configured.Username || resolved.Password == configured.Password { | ||||
| 				t.Errorf("%s -> %v == %v\n", registry, resolved, configured) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,274 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/docker/docker/opts" | ||||
| 	flag "github.com/docker/docker/pkg/mflag" | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| // ServiceOptions holds command line options.
 | ||||
| type ServiceOptions struct { | ||||
| 	Mirrors            []string `json:"registry-mirrors,omitempty"` | ||||
| 	InsecureRegistries []string `json:"insecure-registries,omitempty"` | ||||
| 
 | ||||
| 	// V2Only controls access to legacy registries.  If it is set to true via the
 | ||||
| 	// command line flag the daemon will not attempt to contact v1 legacy registries
 | ||||
| 	V2Only bool `json:"disable-legacy-registry,omitempty"` | ||||
| } | ||||
| 
 | ||||
| // serviceConfig holds daemon configuration for the registry service.
 | ||||
| type serviceConfig struct { | ||||
| 	registrytypes.ServiceConfig | ||||
| 	V2Only bool | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	// DefaultNamespace is the default namespace
 | ||||
| 	DefaultNamespace = "docker.io" | ||||
| 	// DefaultRegistryVersionHeader is the name of the default HTTP header
 | ||||
| 	// that carries Registry version info
 | ||||
| 	DefaultRegistryVersionHeader = "Docker-Distribution-Api-Version" | ||||
| 
 | ||||
| 	// IndexServer is the v1 registry server used for user auth + account creation
 | ||||
| 	IndexServer = DefaultV1Registry.String() + "/v1/" | ||||
| 	// IndexName is the name of the index
 | ||||
| 	IndexName = "docker.io" | ||||
| 
 | ||||
| 	// NotaryServer is the endpoint serving the Notary trust server
 | ||||
| 	NotaryServer = "https://notary.docker.io" | ||||
| 
 | ||||
| 	// DefaultV1Registry is the URI of the default v1 registry
 | ||||
| 	DefaultV1Registry = &url.URL{ | ||||
| 		Scheme: "https", | ||||
| 		Host:   "index.docker.io", | ||||
| 	} | ||||
| 
 | ||||
| 	// DefaultV2Registry is the URI of the default v2 registry
 | ||||
| 	DefaultV2Registry = &url.URL{ | ||||
| 		Scheme: "https", | ||||
| 		Host:   "registry-1.docker.io", | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// ErrInvalidRepositoryName is an error returned if the repository name did
 | ||||
| 	// not have the correct form
 | ||||
| 	ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")") | ||||
| 
 | ||||
| 	emptyServiceConfig = newServiceConfig(ServiceOptions{}) | ||||
| ) | ||||
| 
 | ||||
| // for mocking in unit tests
 | ||||
| var lookupIP = net.LookupIP | ||||
| 
 | ||||
| // InstallCliFlags adds command-line options to the top-level flag parser for
 | ||||
| // the current process.
 | ||||
| func (options *ServiceOptions) InstallCliFlags(cmd *flag.FlagSet, usageFn func(string) string) { | ||||
| 	mirrors := opts.NewNamedListOptsRef("registry-mirrors", &options.Mirrors, ValidateMirror) | ||||
| 	cmd.Var(mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror")) | ||||
| 
 | ||||
| 	insecureRegistries := opts.NewNamedListOptsRef("insecure-registries", &options.InsecureRegistries, ValidateIndexName) | ||||
| 	cmd.Var(insecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication")) | ||||
| 
 | ||||
| 	cmd.BoolVar(&options.V2Only, []string{"-disable-legacy-registry"}, false, usageFn("Disable contacting legacy registries")) | ||||
| } | ||||
| 
 | ||||
| // newServiceConfig returns a new instance of ServiceConfig
 | ||||
| func newServiceConfig(options ServiceOptions) *serviceConfig { | ||||
| 	// Localhost is by default considered as an insecure registry
 | ||||
| 	// This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker).
 | ||||
| 	//
 | ||||
| 	// TODO: should we deprecate this once it is easier for people to set up a TLS registry or change
 | ||||
| 	// daemon flags on boot2docker?
 | ||||
| 	options.InsecureRegistries = append(options.InsecureRegistries, "127.0.0.0/8") | ||||
| 
 | ||||
| 	config := &serviceConfig{ | ||||
| 		ServiceConfig: registrytypes.ServiceConfig{ | ||||
| 			InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0), | ||||
| 			IndexConfigs:          make(map[string]*registrytypes.IndexInfo, 0), | ||||
| 			// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
 | ||||
| 			// and Mirrors are only for the official registry anyways.
 | ||||
| 			Mirrors: options.Mirrors, | ||||
| 		}, | ||||
| 		V2Only: options.V2Only, | ||||
| 	} | ||||
| 	// Split --insecure-registry into CIDR and registry-specific settings.
 | ||||
| 	for _, r := range options.InsecureRegistries { | ||||
| 		// Check if CIDR was passed to --insecure-registry
 | ||||
| 		_, ipnet, err := net.ParseCIDR(r) | ||||
| 		if err == nil { | ||||
| 			// Valid CIDR.
 | ||||
| 			config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*registrytypes.NetIPNet)(ipnet)) | ||||
| 		} else { | ||||
| 			// Assume `host:port` if not CIDR.
 | ||||
| 			config.IndexConfigs[r] = ®istrytypes.IndexInfo{ | ||||
| 				Name:     r, | ||||
| 				Mirrors:  make([]string, 0), | ||||
| 				Secure:   false, | ||||
| 				Official: false, | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Configure public registry.
 | ||||
| 	config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{ | ||||
| 		Name:     IndexName, | ||||
| 		Mirrors:  config.Mirrors, | ||||
| 		Secure:   true, | ||||
| 		Official: true, | ||||
| 	} | ||||
| 
 | ||||
| 	return config | ||||
| } | ||||
| 
 | ||||
| // isSecureIndex returns false if the provided indexName is part of the list of insecure registries
 | ||||
| // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
 | ||||
| //
 | ||||
| // The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
 | ||||
| // If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
 | ||||
| // insecure.
 | ||||
| //
 | ||||
| // indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
 | ||||
| // or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained
 | ||||
| // in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
 | ||||
| // of insecureRegistries.
 | ||||
| func isSecureIndex(config *serviceConfig, indexName string) bool { | ||||
| 	// Check for configured index, first.  This is needed in case isSecureIndex
 | ||||
| 	// is called from anything besides newIndexInfo, in order to honor per-index configurations.
 | ||||
| 	if index, ok := config.IndexConfigs[indexName]; ok { | ||||
| 		return index.Secure | ||||
| 	} | ||||
| 
 | ||||
| 	host, _, err := net.SplitHostPort(indexName) | ||||
| 	if err != nil { | ||||
| 		// assume indexName is of the form `host` without the port and go on.
 | ||||
| 		host = indexName | ||||
| 	} | ||||
| 
 | ||||
| 	addrs, err := lookupIP(host) | ||||
| 	if err != nil { | ||||
| 		ip := net.ParseIP(host) | ||||
| 		if ip != nil { | ||||
| 			addrs = []net.IP{ip} | ||||
| 		} | ||||
| 
 | ||||
| 		// if ip == nil, then `host` is neither an IP nor it could be looked up,
 | ||||
| 		// either because the index is unreachable, or because the index is behind an HTTP proxy.
 | ||||
| 		// So, len(addrs) == 0 and we're not aborting.
 | ||||
| 	} | ||||
| 
 | ||||
| 	// Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined.
 | ||||
| 	for _, addr := range addrs { | ||||
| 		for _, ipnet := range config.InsecureRegistryCIDRs { | ||||
| 			// check if the addr falls in the subnet
 | ||||
| 			if (*net.IPNet)(ipnet).Contains(addr) { | ||||
| 				return false | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| // ValidateMirror validates an HTTP(S) registry mirror
 | ||||
| func ValidateMirror(val string) (string, error) { | ||||
| 	uri, err := url.Parse(val) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("%s is not a valid URI", val) | ||||
| 	} | ||||
| 
 | ||||
| 	if uri.Scheme != "http" && uri.Scheme != "https" { | ||||
| 		return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme) | ||||
| 	} | ||||
| 
 | ||||
| 	if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" { | ||||
| 		return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI") | ||||
| 	} | ||||
| 
 | ||||
| 	return fmt.Sprintf("%s://%s/", uri.Scheme, uri.Host), nil | ||||
| } | ||||
| 
 | ||||
| // ValidateIndexName validates an index name.
 | ||||
| func ValidateIndexName(val string) (string, error) { | ||||
| 	if val == reference.LegacyDefaultHostname { | ||||
| 		val = reference.DefaultHostname | ||||
| 	} | ||||
| 	if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { | ||||
| 		return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val) | ||||
| 	} | ||||
| 	return val, nil | ||||
| } | ||||
| 
 | ||||
| func validateNoScheme(reposName string) error { | ||||
| 	if strings.Contains(reposName, "://") { | ||||
| 		// It cannot contain a scheme!
 | ||||
| 		return ErrInvalidRepositoryName | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // newIndexInfo returns IndexInfo configuration from indexName
 | ||||
| func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) { | ||||
| 	var err error | ||||
| 	indexName, err = ValidateIndexName(indexName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Return any configured index info, first.
 | ||||
| 	if index, ok := config.IndexConfigs[indexName]; ok { | ||||
| 		return index, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Construct a non-configured index info.
 | ||||
| 	index := ®istrytypes.IndexInfo{ | ||||
| 		Name:     indexName, | ||||
| 		Mirrors:  make([]string, 0), | ||||
| 		Official: false, | ||||
| 	} | ||||
| 	index.Secure = isSecureIndex(config, indexName) | ||||
| 	return index, nil | ||||
| } | ||||
| 
 | ||||
| // GetAuthConfigKey special-cases using the full index address of the official
 | ||||
| // index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
 | ||||
| func GetAuthConfigKey(index *registrytypes.IndexInfo) string { | ||||
| 	if index.Official { | ||||
| 		return IndexServer | ||||
| 	} | ||||
| 	return index.Name | ||||
| } | ||||
| 
 | ||||
| // newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
 | ||||
| func newRepositoryInfo(config *serviceConfig, name reference.Named) (*RepositoryInfo, error) { | ||||
| 	index, err := newIndexInfo(config, name.Hostname()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	official := !strings.ContainsRune(name.Name(), '/') | ||||
| 	return &RepositoryInfo{name, index, official}, nil | ||||
| } | ||||
| 
 | ||||
| // ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but
 | ||||
| // lacks registry configuration.
 | ||||
| func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { | ||||
| 	return newRepositoryInfo(emptyServiceConfig, reposName) | ||||
| } | ||||
| 
 | ||||
| // ParseSearchIndexInfo will use repository name to get back an indexInfo.
 | ||||
| func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) { | ||||
| 	indexName, _ := splitReposSearchTerm(reposName) | ||||
| 
 | ||||
| 	indexInfo, err := newIndexInfo(emptyServiceConfig, indexName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return indexInfo, nil | ||||
| } | ||||
|  | @ -0,0 +1,49 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestValidateMirror(t *testing.T) { | ||||
| 	valid := []string{ | ||||
| 		"http://mirror-1.com", | ||||
| 		"https://mirror-1.com", | ||||
| 		"http://localhost", | ||||
| 		"https://localhost", | ||||
| 		"http://localhost:5000", | ||||
| 		"https://localhost:5000", | ||||
| 		"http://127.0.0.1", | ||||
| 		"https://127.0.0.1", | ||||
| 		"http://127.0.0.1:5000", | ||||
| 		"https://127.0.0.1:5000", | ||||
| 	} | ||||
| 
 | ||||
| 	invalid := []string{ | ||||
| 		"!invalid!://%as%", | ||||
| 		"ftp://mirror-1.com", | ||||
| 		"http://mirror-1.com/", | ||||
| 		"http://mirror-1.com/?q=foo", | ||||
| 		"http://mirror-1.com/v1/", | ||||
| 		"http://mirror-1.com/v1/?q=foo", | ||||
| 		"http://mirror-1.com/v1/?q=foo#frag", | ||||
| 		"http://mirror-1.com?q=foo", | ||||
| 		"https://mirror-1.com#frag", | ||||
| 		"https://mirror-1.com/", | ||||
| 		"https://mirror-1.com/#frag", | ||||
| 		"https://mirror-1.com/v1/", | ||||
| 		"https://mirror-1.com/v1/#", | ||||
| 		"https://mirror-1.com?q", | ||||
| 	} | ||||
| 
 | ||||
| 	for _, address := range valid { | ||||
| 		if ret, err := ValidateMirror(address); err != nil || ret == "" { | ||||
| 			t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, address := range invalid { | ||||
| 		if ret, err := ValidateMirror(address); err == nil || ret != "" { | ||||
| 			t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| // +build !windows
 | ||||
| 
 | ||||
| package registry | ||||
| 
 | ||||
| var ( | ||||
| 	// CertsDir is the directory where certificates are stored
 | ||||
| 	CertsDir = "/etc/docker/certs.d" | ||||
| ) | ||||
| 
 | ||||
| // cleanPath is used to ensure that a directory name is valid on the target
 | ||||
| // platform. It will be passed in something *similar* to a URL such as
 | ||||
| // https:/index.docker.io/v1. Not all platforms support directory names
 | ||||
| // which contain those characters (such as : on Windows)
 | ||||
| func cleanPath(s string) string { | ||||
| 	return s | ||||
| } | ||||
|  | @ -0,0 +1,18 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // CertsDir is the directory where certificates are stored
 | ||||
| var CertsDir = os.Getenv("programdata") + `\docker\certs.d` | ||||
| 
 | ||||
| // cleanPath is used to ensure that a directory name is valid on the target
 | ||||
| // platform. It will be passed in something *similar* to a URL such as
 | ||||
| // https:\index.docker.io\v1. Not all platforms support directory names
 | ||||
| // which contain those characters (such as : on Windows)
 | ||||
| func cleanPath(s string) string { | ||||
| 	return filepath.FromSlash(strings.Replace(s, ":", "", -1)) | ||||
| } | ||||
|  | @ -0,0 +1,78 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestEndpointParse(t *testing.T) { | ||||
| 	testData := []struct { | ||||
| 		str      string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{IndexServer, IndexServer}, | ||||
| 		{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"}, | ||||
| 		{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"}, | ||||
| 		{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"}, | ||||
| 		{"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 { | ||||
| 		e, err := newV1EndpointFromStr(td.str, nil, "", nil) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("%q: %s", td.str, err) | ||||
| 		} | ||||
| 		if e == nil { | ||||
| 			t.Logf("something's fishy, endpoint for %q is nil", td.str) | ||||
| 			continue | ||||
| 		} | ||||
| 		if e.String() != td.expected { | ||||
| 			t.Errorf("expected %q, got %q", td.expected, e.String()) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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
 | ||||
| // to be a valid v1 registry endpoint
 | ||||
| func TestValidateEndpoint(t *testing.T) { | ||||
| 	requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`) | ||||
| 		w.WriteHeader(http.StatusUnauthorized) | ||||
| 	}) | ||||
| 
 | ||||
| 	// Make a test server which should validate as a v1 server.
 | ||||
| 	testServer := httptest.NewServer(requireBasicAuthHandler) | ||||
| 	defer testServer.Close() | ||||
| 
 | ||||
| 	testServerURL, err := url.Parse(testServer.URL) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	testEndpoint := V1Endpoint{ | ||||
| 		URL:    testServerURL, | ||||
| 		client: HTTPClient(NewTransport(nil)), | ||||
| 	} | ||||
| 
 | ||||
| 	if err = validateEndpoint(&testEndpoint); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	if testEndpoint.URL.Scheme != "http" { | ||||
| 		t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String()) | ||||
| 	} | ||||
| } | ||||
|  | @ -0,0 +1,198 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/registry/client/transport" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| // V1Endpoint stores basic information about a V1 registry endpoint.
 | ||||
| type V1Endpoint struct { | ||||
| 	client   *http.Client | ||||
| 	URL      *url.URL | ||||
| 	IsSecure bool | ||||
| } | ||||
| 
 | ||||
| // NewV1Endpoint parses the given address to return a registry endpoint.
 | ||||
| func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { | ||||
| 	tlsConfig, err := newTLSConfig(index.Name, index.Secure) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := validateEndpoint(endpoint); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return endpoint, nil | ||||
| } | ||||
| 
 | ||||
| func validateEndpoint(endpoint *V1Endpoint) error { | ||||
| 	logrus.Debugf("pinging registry endpoint %s", endpoint) | ||||
| 
 | ||||
| 	// Try HTTPS ping to registry
 | ||||
| 	endpoint.URL.Scheme = "https" | ||||
| 	if _, err := endpoint.Ping(); err != nil { | ||||
| 		if endpoint.IsSecure { | ||||
| 			// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
 | ||||
| 			// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
 | ||||
| 			return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) | ||||
| 		} | ||||
| 
 | ||||
| 		// If registry is insecure and HTTPS failed, fallback to HTTP.
 | ||||
| 		logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err) | ||||
| 		endpoint.URL.Scheme = "http" | ||||
| 
 | ||||
| 		var err2 error | ||||
| 		if _, err2 = endpoint.Ping(); err2 == nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { | ||||
| 	endpoint := &V1Endpoint{ | ||||
| 		IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify), | ||||
| 		URL:      new(url.URL), | ||||
| 	} | ||||
| 
 | ||||
| 	*endpoint.URL = address | ||||
| 
 | ||||
| 	// TODO(tiborvass): make sure a ConnectTimeout transport is used
 | ||||
| 	tr := NewTransport(tlsConfig) | ||||
| 	endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...)) | ||||
| 	return endpoint, nil | ||||
| } | ||||
| 
 | ||||
| // 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://") { | ||||
| 		address = "https://" + address | ||||
| 	} | ||||
| 
 | ||||
| 	address, err := trimV1Address(address) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	uri, err := url.Parse(address) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return endpoint, nil | ||||
| } | ||||
| 
 | ||||
| // Get the formatted URL for the root of this registry Endpoint
 | ||||
| func (e *V1Endpoint) String() string { | ||||
| 	return e.URL.String() + "/v1/" | ||||
| } | ||||
| 
 | ||||
| // Path returns a formatted string for the URL
 | ||||
| // of this endpoint with the given path appended.
 | ||||
| func (e *V1Endpoint) Path(path string) string { | ||||
| 	return e.URL.String() + "/v1/" + path | ||||
| } | ||||
| 
 | ||||
| // Ping returns a PingResult which indicates whether the registry is standalone or not.
 | ||||
| func (e *V1Endpoint) Ping() (PingResult, error) { | ||||
| 	logrus.Debugf("attempting v1 ping for registry endpoint %s", e) | ||||
| 
 | ||||
| 	if e.String() == IndexServer { | ||||
| 		// Skip the check, we know this one is valid
 | ||||
| 		// (and we never want to fallback to http in case of error)
 | ||||
| 		return PingResult{Standalone: false}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", e.Path("_ping"), nil) | ||||
| 	if err != nil { | ||||
| 		return PingResult{Standalone: false}, err | ||||
| 	} | ||||
| 
 | ||||
| 	resp, err := e.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return PingResult{Standalone: false}, err | ||||
| 	} | ||||
| 
 | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	jsonString, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	// If the header is absent, we assume true for compatibility with earlier
 | ||||
| 	// versions of the registry. default to true
 | ||||
| 	info := PingResult{ | ||||
| 		Standalone: true, | ||||
| 	} | ||||
| 	if err := json.Unmarshal(jsonString, &info); err != nil { | ||||
| 		logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err) | ||||
| 		// don't stop here. Just assume sane defaults
 | ||||
| 	} | ||||
| 	if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" { | ||||
| 		logrus.Debugf("Registry version header: '%s'", hdr) | ||||
| 		info.Version = hdr | ||||
| 	} | ||||
| 	logrus.Debugf("PingResult.Version: %q", info.Version) | ||||
| 
 | ||||
| 	standalone := resp.Header.Get("X-Docker-Registry-Standalone") | ||||
| 	logrus.Debugf("Registry standalone header: '%s'", standalone) | ||||
| 	// Accepted values are "true" (case-insensitive) and "1".
 | ||||
| 	if strings.EqualFold(standalone, "true") || standalone == "1" { | ||||
| 		info.Standalone = true | ||||
| 	} else if len(standalone) > 0 { | ||||
| 		// there is a header set, and it is not "true" or "1", so assume fails
 | ||||
| 		info.Standalone = false | ||||
| 	} | ||||
| 	logrus.Debugf("PingResult.Standalone: %t", info.Standalone) | ||||
| 	return info, nil | ||||
| } | ||||
|  | @ -0,0 +1,68 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/digest" | ||||
| ) | ||||
| 
 | ||||
| // Reference represents a tag or digest within a repository
 | ||||
| type Reference interface { | ||||
| 	// HasDigest returns whether the reference has a verifiable
 | ||||
| 	// content addressable reference which may be considered secure.
 | ||||
| 	HasDigest() bool | ||||
| 
 | ||||
| 	// ImageName returns an image name for the given repository
 | ||||
| 	ImageName(string) string | ||||
| 
 | ||||
| 	// Returns a string representation of the reference
 | ||||
| 	String() string | ||||
| } | ||||
| 
 | ||||
| type tagReference struct { | ||||
| 	tag string | ||||
| } | ||||
| 
 | ||||
| func (tr tagReference) HasDigest() bool { | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func (tr tagReference) ImageName(repo string) string { | ||||
| 	return repo + ":" + tr.tag | ||||
| } | ||||
| 
 | ||||
| func (tr tagReference) String() string { | ||||
| 	return tr.tag | ||||
| } | ||||
| 
 | ||||
| type digestReference struct { | ||||
| 	digest digest.Digest | ||||
| } | ||||
| 
 | ||||
| func (dr digestReference) HasDigest() bool { | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func (dr digestReference) ImageName(repo string) string { | ||||
| 	return repo + "@" + dr.String() | ||||
| } | ||||
| 
 | ||||
| func (dr digestReference) String() string { | ||||
| 	return dr.digest.String() | ||||
| } | ||||
| 
 | ||||
| // ParseReference parses a reference into either a digest or tag reference
 | ||||
| func ParseReference(ref string) Reference { | ||||
| 	if strings.Contains(ref, ":") { | ||||
| 		dgst, err := digest.ParseDigest(ref) | ||||
| 		if err == nil { | ||||
| 			return digestReference{digest: dgst} | ||||
| 		} | ||||
| 	} | ||||
| 	return tagReference{tag: ref} | ||||
| } | ||||
| 
 | ||||
| // DigestReference creates a digest reference using a digest
 | ||||
| func DigestReference(dgst digest.Digest) Reference { | ||||
| 	return digestReference{digest: dgst} | ||||
| } | ||||
|  | @ -0,0 +1,190 @@ | |||
| // Package registry contains client primitives to interact with a remote Docker registry.
 | ||||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/registry/client/transport" | ||||
| 	"github.com/docker/go-connections/sockets" | ||||
| 	"github.com/docker/go-connections/tlsconfig" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// ErrAlreadyExists is an error returned if an image being pushed
 | ||||
| 	// already exists on the remote side
 | ||||
| 	ErrAlreadyExists = errors.New("Image already exists") | ||||
| ) | ||||
| 
 | ||||
| func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) { | ||||
| 	// PreferredServerCipherSuites should have no effect
 | ||||
| 	tlsConfig := tlsconfig.ServerDefault | ||||
| 
 | ||||
| 	tlsConfig.InsecureSkipVerify = !isSecure | ||||
| 
 | ||||
| 	if isSecure && CertsDir != "" { | ||||
| 		hostDir := filepath.Join(CertsDir, cleanPath(hostname)) | ||||
| 		logrus.Debugf("hostDir: %s", hostDir) | ||||
| 		if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return &tlsConfig, nil | ||||
| } | ||||
| 
 | ||||
| func hasFile(files []os.FileInfo, name string) bool { | ||||
| 	for _, f := range files { | ||||
| 		if f.Name() == name { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // ReadCertsDirectory reads the directory for TLS certificates
 | ||||
| // including roots and certificate pairs and updates the
 | ||||
| // provided TLS configuration.
 | ||||
| func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { | ||||
| 	fs, err := ioutil.ReadDir(directory) | ||||
| 	if err != nil && !os.IsNotExist(err) { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, f := range fs { | ||||
| 		if strings.HasSuffix(f.Name(), ".crt") { | ||||
| 			if tlsConfig.RootCAs == nil { | ||||
| 				// TODO(dmcgowan): Copy system pool
 | ||||
| 				tlsConfig.RootCAs = x509.NewCertPool() | ||||
| 			} | ||||
| 			logrus.Debugf("crt: %s", filepath.Join(directory, f.Name())) | ||||
| 			data, err := ioutil.ReadFile(filepath.Join(directory, f.Name())) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			tlsConfig.RootCAs.AppendCertsFromPEM(data) | ||||
| 		} | ||||
| 		if strings.HasSuffix(f.Name(), ".cert") { | ||||
| 			certName := f.Name() | ||||
| 			keyName := certName[:len(certName)-5] + ".key" | ||||
| 			logrus.Debugf("cert: %s", filepath.Join(directory, f.Name())) | ||||
| 			if !hasFile(fs, keyName) { | ||||
| 				return fmt.Errorf("Missing key %s for client certificate %s. Note that CA certificates should use the extension .crt.", keyName, certName) | ||||
| 			} | ||||
| 			cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			tlsConfig.Certificates = append(tlsConfig.Certificates, cert) | ||||
| 		} | ||||
| 		if strings.HasSuffix(f.Name(), ".key") { | ||||
| 			keyName := f.Name() | ||||
| 			certName := keyName[:len(keyName)-4] + ".cert" | ||||
| 			logrus.Debugf("key: %s", filepath.Join(directory, f.Name())) | ||||
| 			if !hasFile(fs, certName) { | ||||
| 				return fmt.Errorf("Missing client certificate %s for key %s", certName, keyName) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // DockerHeaders returns request modifiers with a User-Agent and metaHeaders
 | ||||
| func DockerHeaders(userAgent string, metaHeaders http.Header) []transport.RequestModifier { | ||||
| 	modifiers := []transport.RequestModifier{} | ||||
| 	if userAgent != "" { | ||||
| 		modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{ | ||||
| 			"User-Agent": []string{userAgent}, | ||||
| 		})) | ||||
| 	} | ||||
| 	if metaHeaders != nil { | ||||
| 		modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) | ||||
| 	} | ||||
| 	return modifiers | ||||
| } | ||||
| 
 | ||||
| // HTTPClient returns an HTTP client structure which uses the given transport
 | ||||
| // and contains the necessary headers for redirected requests
 | ||||
| func HTTPClient(transport http.RoundTripper) *http.Client { | ||||
| 	return &http.Client{ | ||||
| 		Transport:     transport, | ||||
| 		CheckRedirect: addRequiredHeadersToRedirectedRequests, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func trustedLocation(req *http.Request) bool { | ||||
| 	var ( | ||||
| 		trusteds = []string{"docker.com", "docker.io"} | ||||
| 		hostname = strings.SplitN(req.Host, ":", 2)[0] | ||||
| 	) | ||||
| 	if req.URL.Scheme != "https" { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	for _, trusted := range trusteds { | ||||
| 		if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
 | ||||
| // for redirected requests
 | ||||
| func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { | ||||
| 	if via != nil && via[0] != nil { | ||||
| 		if trustedLocation(req) && trustedLocation(via[0]) { | ||||
| 			req.Header = via[0].Header | ||||
| 			return nil | ||||
| 		} | ||||
| 		for k, v := range via[0].Header { | ||||
| 			if k != "Authorization" { | ||||
| 				for _, vv := range v { | ||||
| 					req.Header.Add(k, vv) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
 | ||||
| // default TLS configuration.
 | ||||
| func NewTransport(tlsConfig *tls.Config) *http.Transport { | ||||
| 	if tlsConfig == nil { | ||||
| 		var cfg = tlsconfig.ServerDefault | ||||
| 		tlsConfig = &cfg | ||||
| 	} | ||||
| 
 | ||||
| 	direct := &net.Dialer{ | ||||
| 		Timeout:   30 * time.Second, | ||||
| 		KeepAlive: 30 * time.Second, | ||||
| 		DualStack: true, | ||||
| 	} | ||||
| 
 | ||||
| 	base := &http.Transport{ | ||||
| 		Proxy:               http.ProxyFromEnvironment, | ||||
| 		Dial:                direct.Dial, | ||||
| 		TLSHandshakeTimeout: 10 * time.Second, | ||||
| 		TLSClientConfig:     tlsConfig, | ||||
| 		// TODO(dmcgowan): Call close idle connections when complete and use keep alive
 | ||||
| 		DisableKeepAlives: true, | ||||
| 	} | ||||
| 
 | ||||
| 	proxyDialer, err := sockets.DialerFromEnvironment(direct) | ||||
| 	if err == nil { | ||||
| 		base.Dial = proxyDialer.Dial | ||||
| 	} | ||||
| 	return base | ||||
| } | ||||
|  | @ -0,0 +1,476 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| 	"github.com/gorilla/mux" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	testHTTPServer  *httptest.Server | ||||
| 	testHTTPSServer *httptest.Server | ||||
| 	testLayers      = map[string]map[string]string{ | ||||
| 		"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": { | ||||
| 			"json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", | ||||
| 				"comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00", | ||||
| 				"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, | ||||
| 				"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, | ||||
| 				"Tty":false,"OpenStdin":false,"StdinOnce":false, | ||||
| 				"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, | ||||
| 				"VolumesFrom":"","Entrypoint":null},"Size":424242}`, | ||||
| 			"checksum_simple": "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", | ||||
| 			"checksum_tarsum": "tarsum+sha256:4409a0685741ca86d38df878ed6f8cbba4c99de5dc73cd71aef04be3bb70be7c", | ||||
| 			"ancestry":        `["77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, | ||||
| 			"layer": string([]byte{ | ||||
| 				0x1f, 0x8b, 0x08, 0x08, 0x0e, 0xb0, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, | ||||
| 				0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd2, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, | ||||
| 				0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0xed, 0x38, 0x4e, 0xce, 0x13, 0x44, 0x2b, 0x66, | ||||
| 				0x62, 0x24, 0x8e, 0x4f, 0xa0, 0x15, 0x63, 0xb6, 0x20, 0x21, 0xfc, 0x96, 0xbf, 0x78, | ||||
| 				0xb0, 0xf5, 0x1d, 0x16, 0x98, 0x8e, 0x88, 0x8a, 0x2a, 0xbe, 0x33, 0xef, 0x49, 0x31, | ||||
| 				0xed, 0x79, 0x40, 0x8e, 0x5c, 0x44, 0x85, 0x88, 0x33, 0x12, 0x73, 0x2c, 0x02, 0xa8, | ||||
| 				0xf0, 0x05, 0xf7, 0x66, 0xf5, 0xd6, 0x57, 0x69, 0xd7, 0x7a, 0x19, 0xcd, 0xf5, 0xb1, | ||||
| 				0x6d, 0x1b, 0x1f, 0xf9, 0xba, 0xe3, 0x93, 0x3f, 0x22, 0x2c, 0xb6, 0x36, 0x0b, 0xf6, | ||||
| 				0xb0, 0xa9, 0xfd, 0xe7, 0x94, 0x46, 0xfd, 0xeb, 0xd1, 0x7f, 0x2c, 0xc4, 0xd2, 0xfb, | ||||
| 				0x97, 0xfe, 0x02, 0x80, 0xe4, 0xfd, 0x4f, 0x77, 0xae, 0x6d, 0x3d, 0x81, 0x73, 0xce, | ||||
| 				0xb9, 0x7f, 0xf3, 0x04, 0x41, 0xc1, 0xab, 0xc6, 0x00, 0x0a, 0x00, 0x00, | ||||
| 			}), | ||||
| 		}, | ||||
| 		"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d": { | ||||
| 			"json": `{"id":"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", | ||||
| 				"parent":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", | ||||
| 				"comment":"test base image","created":"2013-03-23T12:55:11.10432-07:00", | ||||
| 				"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0, | ||||
| 				"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false, | ||||
| 				"Tty":false,"OpenStdin":false,"StdinOnce":false, | ||||
| 				"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null, | ||||
| 				"VolumesFrom":"","Entrypoint":null},"Size":424242}`, | ||||
| 			"checksum_simple": "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", | ||||
| 			"checksum_tarsum": "tarsum+sha256:68fdb56fb364f074eec2c9b3f85ca175329c4dcabc4a6a452b7272aa613a07a2", | ||||
| 			"ancestry": `["42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", | ||||
| 				"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`, | ||||
| 			"layer": string([]byte{ | ||||
| 				0x1f, 0x8b, 0x08, 0x08, 0xbd, 0xb3, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65, | ||||
| 				0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd1, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05, | ||||
| 				0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0x9d, 0x38, 0x8e, 0xcf, 0x53, 0x51, 0xaa, 0x56, | ||||
| 				0xea, 0x44, 0x82, 0xc4, 0xf1, 0x09, 0xb4, 0xea, 0x98, 0x2d, 0x48, 0x08, 0xbf, 0xe5, | ||||
| 				0x2f, 0x1e, 0xfc, 0xf5, 0xdd, 0x00, 0xdd, 0x11, 0x91, 0x8a, 0xe0, 0x27, 0xd3, 0x9e, | ||||
| 				0x14, 0xe2, 0x9e, 0x07, 0xf4, 0xc1, 0x2b, 0x0b, 0xfb, 0xa4, 0x82, 0xe4, 0x3d, 0x93, | ||||
| 				0x02, 0x0a, 0x7c, 0xc1, 0x23, 0x97, 0xf1, 0x5e, 0x5f, 0xc9, 0xcb, 0x38, 0xb5, 0xee, | ||||
| 				0xea, 0xd9, 0x3c, 0xb7, 0x4b, 0xbe, 0x7b, 0x9c, 0xf9, 0x23, 0xdc, 0x50, 0x6e, 0xb9, | ||||
| 				0xb8, 0xf2, 0x2c, 0x5d, 0xf7, 0x4f, 0x31, 0xb6, 0xf6, 0x4f, 0xc7, 0xfe, 0x41, 0x55, | ||||
| 				0x63, 0xdd, 0x9f, 0x89, 0x09, 0x90, 0x6c, 0xff, 0xee, 0xae, 0xcb, 0xba, 0x4d, 0x17, | ||||
| 				0x30, 0xc6, 0x18, 0xf3, 0x67, 0x5e, 0xc1, 0xed, 0x21, 0x5d, 0x00, 0x0a, 0x00, 0x00, | ||||
| 			}), | ||||
| 		}, | ||||
| 	} | ||||
| 	testRepositories = map[string]map[string]string{ | ||||
| 		"foo42/bar": { | ||||
| 			"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", | ||||
| 			"test":   "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", | ||||
| 		}, | ||||
| 	} | ||||
| 	mockHosts = map[string][]net.IP{ | ||||
| 		"":            {net.ParseIP("0.0.0.0")}, | ||||
| 		"localhost":   {net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, | ||||
| 		"example.com": {net.ParseIP("42.42.42.42")}, | ||||
| 		"other.com":   {net.ParseIP("43.43.43.43")}, | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	r := mux.NewRouter() | ||||
| 
 | ||||
| 	// /v1/
 | ||||
| 	r.HandleFunc("/v1/_ping", handlerGetPing).Methods("GET") | ||||
| 	r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods("GET") | ||||
| 	r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods("PUT") | ||||
| 	r.HandleFunc("/v1/repositories/{repository:.+}/tags", handlerGetDeleteTags).Methods("GET", "DELETE") | ||||
| 	r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerGetTag).Methods("GET") | ||||
| 	r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerPutTag).Methods("PUT") | ||||
| 	r.HandleFunc("/v1/users{null:.*}", handlerUsers).Methods("GET", "POST", "PUT") | ||||
| 	r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods("GET", "PUT", "DELETE") | ||||
| 	r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods("PUT") | ||||
| 	r.HandleFunc("/v1/search", handlerSearch).Methods("GET") | ||||
| 
 | ||||
| 	// /v2/
 | ||||
| 	r.HandleFunc("/v2/version", handlerGetPing).Methods("GET") | ||||
| 
 | ||||
| 	testHTTPServer = httptest.NewServer(handlerAccessLog(r)) | ||||
| 	testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) | ||||
| 
 | ||||
| 	// override net.LookupIP
 | ||||
| 	lookupIP = func(host string) ([]net.IP, error) { | ||||
| 		if host == "127.0.0.1" { | ||||
| 			// I believe in future Go versions this will fail, so let's fix it later
 | ||||
| 			return net.LookupIP(host) | ||||
| 		} | ||||
| 		for h, addrs := range mockHosts { | ||||
| 			if host == h { | ||||
| 				return addrs, nil | ||||
| 			} | ||||
| 			for _, addr := range addrs { | ||||
| 				if addr.String() == host { | ||||
| 					return []net.IP{addr}, nil | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return nil, errors.New("lookup: no such host") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func handlerAccessLog(handler http.Handler) http.Handler { | ||||
| 	logHandler := func(w http.ResponseWriter, r *http.Request) { | ||||
| 		logrus.Debugf("%s \"%s %s\"", r.RemoteAddr, r.Method, r.URL) | ||||
| 		handler.ServeHTTP(w, r) | ||||
| 	} | ||||
| 	return http.HandlerFunc(logHandler) | ||||
| } | ||||
| 
 | ||||
| func makeURL(req string) string { | ||||
| 	return testHTTPServer.URL + req | ||||
| } | ||||
| 
 | ||||
| func makeHTTPSURL(req string) string { | ||||
| 	return testHTTPSServer.URL + req | ||||
| } | ||||
| 
 | ||||
| func makeIndex(req string) *registrytypes.IndexInfo { | ||||
| 	index := ®istrytypes.IndexInfo{ | ||||
| 		Name: makeURL(req), | ||||
| 	} | ||||
| 	return index | ||||
| } | ||||
| 
 | ||||
| func makeHTTPSIndex(req string) *registrytypes.IndexInfo { | ||||
| 	index := ®istrytypes.IndexInfo{ | ||||
| 		Name: makeHTTPSURL(req), | ||||
| 	} | ||||
| 	return index | ||||
| } | ||||
| 
 | ||||
| func makePublicIndex() *registrytypes.IndexInfo { | ||||
| 	index := ®istrytypes.IndexInfo{ | ||||
| 		Name:     IndexServer, | ||||
| 		Secure:   true, | ||||
| 		Official: true, | ||||
| 	} | ||||
| 	return index | ||||
| } | ||||
| 
 | ||||
| func makeServiceConfig(mirrors []string, insecureRegistries []string) *serviceConfig { | ||||
| 	options := ServiceOptions{ | ||||
| 		Mirrors:            mirrors, | ||||
| 		InsecureRegistries: insecureRegistries, | ||||
| 	} | ||||
| 
 | ||||
| 	return newServiceConfig(options) | ||||
| } | ||||
| 
 | ||||
| func writeHeaders(w http.ResponseWriter) { | ||||
| 	h := w.Header() | ||||
| 	h.Add("Server", "docker-tests/mock") | ||||
| 	h.Add("Expires", "-1") | ||||
| 	h.Add("Content-Type", "application/json") | ||||
| 	h.Add("Pragma", "no-cache") | ||||
| 	h.Add("Cache-Control", "no-cache") | ||||
| 	h.Add("X-Docker-Registry-Version", "0.0.0") | ||||
| 	h.Add("X-Docker-Registry-Config", "mock") | ||||
| } | ||||
| 
 | ||||
| func writeResponse(w http.ResponseWriter, message interface{}, code int) { | ||||
| 	writeHeaders(w) | ||||
| 	w.WriteHeader(code) | ||||
| 	body, err := json.Marshal(message) | ||||
| 	if err != nil { | ||||
| 		io.WriteString(w, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	w.Write(body) | ||||
| } | ||||
| 
 | ||||
| func readJSON(r *http.Request, dest interface{}) error { | ||||
| 	body, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return json.Unmarshal(body, dest) | ||||
| } | ||||
| 
 | ||||
| func apiError(w http.ResponseWriter, message string, code int) { | ||||
| 	body := map[string]string{ | ||||
| 		"error": message, | ||||
| 	} | ||||
| 	writeResponse(w, body, code) | ||||
| } | ||||
| 
 | ||||
| func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { | ||||
| 	if a == b { | ||||
| 		return | ||||
| 	} | ||||
| 	if len(message) == 0 { | ||||
| 		message = fmt.Sprintf("%v != %v", a, b) | ||||
| 	} | ||||
| 	t.Fatal(message) | ||||
| } | ||||
| 
 | ||||
| func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) { | ||||
| 	if a != b { | ||||
| 		return | ||||
| 	} | ||||
| 	if len(message) == 0 { | ||||
| 		message = fmt.Sprintf("%v == %v", a, b) | ||||
| 	} | ||||
| 	t.Fatal(message) | ||||
| } | ||||
| 
 | ||||
| // Similar to assertEqual, but does not stop test
 | ||||
| func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { | ||||
| 	if a == b { | ||||
| 		return | ||||
| 	} | ||||
| 	message := fmt.Sprintf("%v != %v", a, b) | ||||
| 	if len(messagePrefix) != 0 { | ||||
| 		message = messagePrefix + ": " + message | ||||
| 	} | ||||
| 	t.Error(message) | ||||
| } | ||||
| 
 | ||||
| // Similar to assertNotEqual, but does not stop test
 | ||||
| func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) { | ||||
| 	if a != b { | ||||
| 		return | ||||
| 	} | ||||
| 	message := fmt.Sprintf("%v == %v", a, b) | ||||
| 	if len(messagePrefix) != 0 { | ||||
| 		message = messagePrefix + ": " + message | ||||
| 	} | ||||
| 	t.Error(message) | ||||
| } | ||||
| 
 | ||||
| func requiresAuth(w http.ResponseWriter, r *http.Request) bool { | ||||
| 	writeCookie := func() { | ||||
| 		value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()) | ||||
| 		cookie := &http.Cookie{Name: "session", Value: value, MaxAge: 3600} | ||||
| 		http.SetCookie(w, cookie) | ||||
| 		//FIXME(sam): this should be sent only on Index routes
 | ||||
| 		value = fmt.Sprintf("FAKE-TOKEN-%d", time.Now().UnixNano()) | ||||
| 		w.Header().Add("X-Docker-Token", value) | ||||
| 	} | ||||
| 	if len(r.Cookies()) > 0 { | ||||
| 		writeCookie() | ||||
| 		return true | ||||
| 	} | ||||
| 	if len(r.Header.Get("Authorization")) > 0 { | ||||
| 		writeCookie() | ||||
| 		return true | ||||
| 	} | ||||
| 	w.Header().Add("WWW-Authenticate", "token") | ||||
| 	apiError(w, "Wrong auth", 401) | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| func handlerGetPing(w http.ResponseWriter, r *http.Request) { | ||||
| 	writeResponse(w, true, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerGetImage(w http.ResponseWriter, r *http.Request) { | ||||
| 	if !requiresAuth(w, r) { | ||||
| 		return | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	layer, exists := testLayers[vars["image_id"]] | ||||
| 	if !exists { | ||||
| 		http.NotFound(w, r) | ||||
| 		return | ||||
| 	} | ||||
| 	writeHeaders(w) | ||||
| 	layerSize := len(layer["layer"]) | ||||
| 	w.Header().Add("X-Docker-Size", strconv.Itoa(layerSize)) | ||||
| 	io.WriteString(w, layer[vars["action"]]) | ||||
| } | ||||
| 
 | ||||
| func handlerPutImage(w http.ResponseWriter, r *http.Request) { | ||||
| 	if !requiresAuth(w, r) { | ||||
| 		return | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	imageID := vars["image_id"] | ||||
| 	action := vars["action"] | ||||
| 	layer, exists := testLayers[imageID] | ||||
| 	if !exists { | ||||
| 		if action != "json" { | ||||
| 			http.NotFound(w, r) | ||||
| 			return | ||||
| 		} | ||||
| 		layer = make(map[string]string) | ||||
| 		testLayers[imageID] = layer | ||||
| 	} | ||||
| 	if checksum := r.Header.Get("X-Docker-Checksum"); checksum != "" { | ||||
| 		if checksum != layer["checksum_simple"] && checksum != layer["checksum_tarsum"] { | ||||
| 			apiError(w, "Wrong checksum", 400) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 	body, err := ioutil.ReadAll(r.Body) | ||||
| 	if err != nil { | ||||
| 		apiError(w, fmt.Sprintf("Error: %s", err), 500) | ||||
| 		return | ||||
| 	} | ||||
| 	layer[action] = string(body) | ||||
| 	writeResponse(w, true, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) { | ||||
| 	if !requiresAuth(w, r) { | ||||
| 		return | ||||
| 	} | ||||
| 	repositoryName, err := reference.WithName(mux.Vars(r)["repository"]) | ||||
| 	if err != nil { | ||||
| 		apiError(w, "Could not parse repository", 400) | ||||
| 		return | ||||
| 	} | ||||
| 	tags, exists := testRepositories[repositoryName.String()] | ||||
| 	if !exists { | ||||
| 		apiError(w, "Repository not found", 404) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.Method == "DELETE" { | ||||
| 		delete(testRepositories, repositoryName.String()) | ||||
| 		writeResponse(w, true, 200) | ||||
| 		return | ||||
| 	} | ||||
| 	writeResponse(w, tags, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerGetTag(w http.ResponseWriter, r *http.Request) { | ||||
| 	if !requiresAuth(w, r) { | ||||
| 		return | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	repositoryName, err := reference.WithName(vars["repository"]) | ||||
| 	if err != nil { | ||||
| 		apiError(w, "Could not parse repository", 400) | ||||
| 		return | ||||
| 	} | ||||
| 	tagName := vars["tag"] | ||||
| 	tags, exists := testRepositories[repositoryName.String()] | ||||
| 	if !exists { | ||||
| 		apiError(w, "Repository not found", 404) | ||||
| 		return | ||||
| 	} | ||||
| 	tag, exists := tags[tagName] | ||||
| 	if !exists { | ||||
| 		apiError(w, "Tag not found", 404) | ||||
| 		return | ||||
| 	} | ||||
| 	writeResponse(w, tag, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerPutTag(w http.ResponseWriter, r *http.Request) { | ||||
| 	if !requiresAuth(w, r) { | ||||
| 		return | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	repositoryName, err := reference.WithName(vars["repository"]) | ||||
| 	if err != nil { | ||||
| 		apiError(w, "Could not parse repository", 400) | ||||
| 		return | ||||
| 	} | ||||
| 	tagName := vars["tag"] | ||||
| 	tags, exists := testRepositories[repositoryName.String()] | ||||
| 	if !exists { | ||||
| 		tags = make(map[string]string) | ||||
| 		testRepositories[repositoryName.String()] = tags | ||||
| 	} | ||||
| 	tagValue := "" | ||||
| 	readJSON(r, tagValue) | ||||
| 	tags[tagName] = tagValue | ||||
| 	writeResponse(w, true, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerUsers(w http.ResponseWriter, r *http.Request) { | ||||
| 	code := 200 | ||||
| 	if r.Method == "POST" { | ||||
| 		code = 201 | ||||
| 	} else if r.Method == "PUT" { | ||||
| 		code = 204 | ||||
| 	} | ||||
| 	writeResponse(w, "", code) | ||||
| } | ||||
| 
 | ||||
| func handlerImages(w http.ResponseWriter, r *http.Request) { | ||||
| 	u, _ := url.Parse(testHTTPServer.URL) | ||||
| 	w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s 	,  %s ", u.Host, "test.example.com")) | ||||
| 	w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())) | ||||
| 	if r.Method == "PUT" { | ||||
| 		if strings.HasSuffix(r.URL.Path, "images") { | ||||
| 			writeResponse(w, "", 204) | ||||
| 			return | ||||
| 		} | ||||
| 		writeResponse(w, "", 200) | ||||
| 		return | ||||
| 	} | ||||
| 	if r.Method == "DELETE" { | ||||
| 		writeResponse(w, "", 204) | ||||
| 		return | ||||
| 	} | ||||
| 	images := []map[string]string{} | ||||
| 	for imageID, layer := range testLayers { | ||||
| 		image := make(map[string]string) | ||||
| 		image["id"] = imageID | ||||
| 		image["checksum"] = layer["checksum_tarsum"] | ||||
| 		image["Tag"] = "latest" | ||||
| 		images = append(images, image) | ||||
| 	} | ||||
| 	writeResponse(w, images, 200) | ||||
| } | ||||
| 
 | ||||
| func handlerAuth(w http.ResponseWriter, r *http.Request) { | ||||
| 	writeResponse(w, "OK", 200) | ||||
| } | ||||
| 
 | ||||
| func handlerSearch(w http.ResponseWriter, r *http.Request) { | ||||
| 	result := ®istrytypes.SearchResults{ | ||||
| 		Query:      "fakequery", | ||||
| 		NumResults: 1, | ||||
| 		Results:    []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}}, | ||||
| 	} | ||||
| 	writeResponse(w, result, 200) | ||||
| } | ||||
| 
 | ||||
| func TestPing(t *testing.T) { | ||||
| 	res, err := http.Get(makeURL("/v1/_ping")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, res.StatusCode, 200, "") | ||||
| 	assertEqual(t, res.Header.Get("X-Docker-Registry-Config"), "mock", | ||||
| 		"This is not a Mocked Registry") | ||||
| } | ||||
| 
 | ||||
| /* Uncomment this to test Mocked Registry locally with curl | ||||
|  * WARNING: Don't push on the repos uncommented, it'll block the tests | ||||
|  * | ||||
| func TestWait(t *testing.T) { | ||||
| 	logrus.Println("Test HTTP server ready and waiting:", testHTTPServer.URL) | ||||
| 	c := make(chan int) | ||||
| 	<-c | ||||
| } | ||||
| 
 | ||||
| //*/
 | ||||
|  | @ -0,0 +1,873 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/httputil" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/docker/distribution/registry/client/transport" | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	"github.com/docker/engine-api/types" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	token = []string{"fake-token"} | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	imageID = "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d" | ||||
| 	REPO    = "foo42/bar" | ||||
| ) | ||||
| 
 | ||||
| func spawnTestRegistrySession(t *testing.T) *Session { | ||||
| 	authConfig := &types.AuthConfig{} | ||||
| 	endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	userAgent := "docker test client" | ||||
| 	var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log} | ||||
| 	tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(userAgent, nil)...) | ||||
| 	client := HTTPClient(tr) | ||||
| 	r, err := NewSession(client, authConfig, endpoint) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
 | ||||
| 	// header while authenticating, in order to retrieve a token that can be later used to
 | ||||
| 	// perform authenticated actions.
 | ||||
| 	//
 | ||||
| 	// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
 | ||||
| 	// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
 | ||||
| 	//
 | ||||
| 	// Because we know that the client's transport is an `*authTransport` we simply cast it,
 | ||||
| 	// in order to set the internal cached token to the fake token, and thus send that fake token
 | ||||
| 	// upon every subsequent requests.
 | ||||
| 	r.client.Transport.(*authTransport).token = token | ||||
| 	return r | ||||
| } | ||||
| 
 | ||||
| func TestPingRegistryEndpoint(t *testing.T) { | ||||
| 	testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) { | ||||
| 		ep, err := NewV1Endpoint(index, "", nil) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		regInfo, err := ep.Ping() | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 
 | ||||
| 		assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage) | ||||
| 	} | ||||
| 
 | ||||
| 	testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") | ||||
| 	testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)") | ||||
| 	testPing(makePublicIndex(), false, "Expected standalone to be false for public index") | ||||
| } | ||||
| 
 | ||||
| func TestEndpoint(t *testing.T) { | ||||
| 	// Simple wrapper to fail test if err != nil
 | ||||
| 	expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint { | ||||
| 		endpoint, err := NewV1Endpoint(index, "", nil) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		return endpoint | ||||
| 	} | ||||
| 
 | ||||
| 	assertInsecureIndex := func(index *registrytypes.IndexInfo) { | ||||
| 		index.Secure = true | ||||
| 		_, err := NewV1Endpoint(index, "", nil) | ||||
| 		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") | ||||
| 		index.Secure = false | ||||
| 	} | ||||
| 
 | ||||
| 	assertSecureIndex := func(index *registrytypes.IndexInfo) { | ||||
| 		index.Secure = true | ||||
| 		_, err := NewV1Endpoint(index, "", nil) | ||||
| 		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") | ||||
| 		index.Secure = false | ||||
| 	} | ||||
| 
 | ||||
| 	index := ®istrytypes.IndexInfo{} | ||||
| 	index.Name = makeURL("/v1/") | ||||
| 	endpoint := expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) | ||||
| 	assertInsecureIndex(index) | ||||
| 
 | ||||
| 	index.Name = makeURL("") | ||||
| 	endpoint = expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") | ||||
| 	assertInsecureIndex(index) | ||||
| 
 | ||||
| 	httpURL := makeURL("") | ||||
| 	index.Name = strings.SplitN(httpURL, "://", 2)[1] | ||||
| 	endpoint = expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") | ||||
| 	assertInsecureIndex(index) | ||||
| 
 | ||||
| 	index.Name = makeHTTPSURL("/v1/") | ||||
| 	endpoint = expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) | ||||
| 	assertSecureIndex(index) | ||||
| 
 | ||||
| 	index.Name = makeHTTPSURL("") | ||||
| 	endpoint = expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") | ||||
| 	assertSecureIndex(index) | ||||
| 
 | ||||
| 	httpsURL := makeHTTPSURL("") | ||||
| 	index.Name = strings.SplitN(httpsURL, "://", 2)[1] | ||||
| 	endpoint = expandEndpoint(index) | ||||
| 	assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") | ||||
| 	assertSecureIndex(index) | ||||
| 
 | ||||
| 	badEndpoints := []string{ | ||||
| 		"http://127.0.0.1/v1/", | ||||
| 		"https://127.0.0.1/v1/", | ||||
| 		"http://127.0.0.1", | ||||
| 		"https://127.0.0.1", | ||||
| 		"127.0.0.1", | ||||
| 	} | ||||
| 	for _, address := range badEndpoints { | ||||
| 		index.Name = address | ||||
| 		_, err := NewV1Endpoint(index, "", nil) | ||||
| 		checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRemoteHistory(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	hist, err := r.GetRemoteHistory(imageID, makeURL("/v1/")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, len(hist), 2, "Expected 2 images in history") | ||||
| 	assertEqual(t, hist[0], imageID, "Expected "+imageID+"as first ancestry") | ||||
| 	assertEqual(t, hist[1], "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", | ||||
| 		"Unexpected second ancestry") | ||||
| } | ||||
| 
 | ||||
| func TestLookupRemoteImage(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	err := r.LookupRemoteImage(imageID, makeURL("/v1/")) | ||||
| 	assertEqual(t, err, nil, "Expected error of remote lookup to nil") | ||||
| 	if err := r.LookupRemoteImage("abcdef", makeURL("/v1/")); err == nil { | ||||
| 		t.Fatal("Expected error of remote lookup to not nil") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRemoteImageJSON(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	json, size, err := r.GetRemoteImageJSON(imageID, makeURL("/v1/")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, size, int64(154), "Expected size 154") | ||||
| 	if len(json) == 0 { | ||||
| 		t.Fatal("Expected non-empty json") | ||||
| 	} | ||||
| 
 | ||||
| 	_, _, err = r.GetRemoteImageJSON("abcdef", makeURL("/v1/")) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Expected image not found error") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRemoteImageLayer(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	data, err := r.GetRemoteImageLayer(imageID, makeURL("/v1/"), 0) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if data == nil { | ||||
| 		t.Fatal("Expected non-nil data result") | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = r.GetRemoteImageLayer("abcdef", makeURL("/v1/"), 0) | ||||
| 	if err == nil { | ||||
| 		t.Fatal("Expected image not found error") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRemoteTag(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	repoRef, err := reference.ParseNamed(REPO) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	tag, err := r.GetRemoteTag([]string{makeURL("/v1/")}, repoRef, "test") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, tag, imageID, "Expected tag test to map to "+imageID) | ||||
| 
 | ||||
| 	bazRef, err := reference.ParseNamed("foo42/baz") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	_, err = r.GetRemoteTag([]string{makeURL("/v1/")}, bazRef, "foo") | ||||
| 	if err != ErrRepoNotFound { | ||||
| 		t.Fatal("Expected ErrRepoNotFound error when fetching tag for bogus repo") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRemoteTags(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	repoRef, err := reference.ParseNamed(REPO) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	tags, err := r.GetRemoteTags([]string{makeURL("/v1/")}, repoRef) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, len(tags), 2, "Expected two tags") | ||||
| 	assertEqual(t, tags["latest"], imageID, "Expected tag latest to map to "+imageID) | ||||
| 	assertEqual(t, tags["test"], imageID, "Expected tag test to map to "+imageID) | ||||
| 
 | ||||
| 	bazRef, err := reference.ParseNamed("foo42/baz") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	_, err = r.GetRemoteTags([]string{makeURL("/v1/")}, bazRef) | ||||
| 	if err != ErrRepoNotFound { | ||||
| 		t.Fatal("Expected ErrRepoNotFound error when fetching tags for bogus repo") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetRepositoryData(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	parsedURL, err := url.Parse(makeURL("/v1/")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	host := "http://" + parsedURL.Host + "/v1/" | ||||
| 	repoRef, err := reference.ParseNamed(REPO) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	data, err := r.GetRepositoryData(repoRef) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	assertEqual(t, len(data.ImgList), 2, "Expected 2 images in ImgList") | ||||
| 	assertEqual(t, len(data.Endpoints), 2, | ||||
| 		fmt.Sprintf("Expected 2 endpoints in Endpoints, found %d instead", len(data.Endpoints))) | ||||
| 	assertEqual(t, data.Endpoints[0], host, | ||||
| 		fmt.Sprintf("Expected first endpoint to be %s but found %s instead", host, data.Endpoints[0])) | ||||
| 	assertEqual(t, data.Endpoints[1], "http://test.example.com/v1/", | ||||
| 		fmt.Sprintf("Expected first endpoint to be http://test.example.com/v1/ but found %s instead", data.Endpoints[1])) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestPushImageJSONRegistry(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	imgData := &ImgData{ | ||||
| 		ID:       "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", | ||||
| 		Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", | ||||
| 	} | ||||
| 
 | ||||
| 	err := r.PushImageJSONRegistry(imgData, []byte{0x42, 0xdf, 0x0}, makeURL("/v1/")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestPushImageLayerRegistry(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	layer := strings.NewReader("") | ||||
| 	_, _, err := r.PushImageLayerRegistry(imageID, layer, makeURL("/v1/"), []byte{}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestParseRepositoryInfo(t *testing.T) { | ||||
| 	type staticRepositoryInfo struct { | ||||
| 		Index         *registrytypes.IndexInfo | ||||
| 		RemoteName    string | ||||
| 		CanonicalName string | ||||
| 		LocalName     string | ||||
| 		Official      bool | ||||
| 	} | ||||
| 
 | ||||
| 	expectedRepoInfos := map[string]staticRepositoryInfo{ | ||||
| 		"fooo/bar": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "fooo/bar", | ||||
| 			LocalName:     "fooo/bar", | ||||
| 			CanonicalName: "docker.io/fooo/bar", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"library/ubuntu": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "library/ubuntu", | ||||
| 			LocalName:     "ubuntu", | ||||
| 			CanonicalName: "docker.io/library/ubuntu", | ||||
| 			Official:      true, | ||||
| 		}, | ||||
| 		"nonlibrary/ubuntu": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "nonlibrary/ubuntu", | ||||
| 			LocalName:     "nonlibrary/ubuntu", | ||||
| 			CanonicalName: "docker.io/nonlibrary/ubuntu", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"ubuntu": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "library/ubuntu", | ||||
| 			LocalName:     "ubuntu", | ||||
| 			CanonicalName: "docker.io/library/ubuntu", | ||||
| 			Official:      true, | ||||
| 		}, | ||||
| 		"other/library": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "other/library", | ||||
| 			LocalName:     "other/library", | ||||
| 			CanonicalName: "docker.io/other/library", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"127.0.0.1:8000/private/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "127.0.0.1:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "private/moonbase", | ||||
| 			LocalName:     "127.0.0.1:8000/private/moonbase", | ||||
| 			CanonicalName: "127.0.0.1:8000/private/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"127.0.0.1:8000/privatebase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "127.0.0.1:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "privatebase", | ||||
| 			LocalName:     "127.0.0.1:8000/privatebase", | ||||
| 			CanonicalName: "127.0.0.1:8000/privatebase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"localhost:8000/private/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "localhost:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "private/moonbase", | ||||
| 			LocalName:     "localhost:8000/private/moonbase", | ||||
| 			CanonicalName: "localhost:8000/private/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"localhost:8000/privatebase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "localhost:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "privatebase", | ||||
| 			LocalName:     "localhost:8000/privatebase", | ||||
| 			CanonicalName: "localhost:8000/privatebase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"example.com/private/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "example.com", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "private/moonbase", | ||||
| 			LocalName:     "example.com/private/moonbase", | ||||
| 			CanonicalName: "example.com/private/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"example.com/privatebase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "example.com", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "privatebase", | ||||
| 			LocalName:     "example.com/privatebase", | ||||
| 			CanonicalName: "example.com/privatebase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"example.com:8000/private/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "example.com:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "private/moonbase", | ||||
| 			LocalName:     "example.com:8000/private/moonbase", | ||||
| 			CanonicalName: "example.com:8000/private/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"example.com:8000/privatebase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "example.com:8000", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "privatebase", | ||||
| 			LocalName:     "example.com:8000/privatebase", | ||||
| 			CanonicalName: "example.com:8000/privatebase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"localhost/private/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "localhost", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "private/moonbase", | ||||
| 			LocalName:     "localhost/private/moonbase", | ||||
| 			CanonicalName: "localhost/private/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"localhost/privatebase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     "localhost", | ||||
| 				Official: false, | ||||
| 			}, | ||||
| 			RemoteName:    "privatebase", | ||||
| 			LocalName:     "localhost/privatebase", | ||||
| 			CanonicalName: "localhost/privatebase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		IndexName + "/public/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "public/moonbase", | ||||
| 			LocalName:     "public/moonbase", | ||||
| 			CanonicalName: "docker.io/public/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"index." + IndexName + "/public/moonbase": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "public/moonbase", | ||||
| 			LocalName:     "public/moonbase", | ||||
| 			CanonicalName: "docker.io/public/moonbase", | ||||
| 			Official:      false, | ||||
| 		}, | ||||
| 		"ubuntu-12.04-base": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "library/ubuntu-12.04-base", | ||||
| 			LocalName:     "ubuntu-12.04-base", | ||||
| 			CanonicalName: "docker.io/library/ubuntu-12.04-base", | ||||
| 			Official:      true, | ||||
| 		}, | ||||
| 		IndexName + "/ubuntu-12.04-base": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "library/ubuntu-12.04-base", | ||||
| 			LocalName:     "ubuntu-12.04-base", | ||||
| 			CanonicalName: "docker.io/library/ubuntu-12.04-base", | ||||
| 			Official:      true, | ||||
| 		}, | ||||
| 		"index." + IndexName + "/ubuntu-12.04-base": { | ||||
| 			Index: ®istrytypes.IndexInfo{ | ||||
| 				Name:     IndexName, | ||||
| 				Official: true, | ||||
| 			}, | ||||
| 			RemoteName:    "library/ubuntu-12.04-base", | ||||
| 			LocalName:     "ubuntu-12.04-base", | ||||
| 			CanonicalName: "docker.io/library/ubuntu-12.04-base", | ||||
| 			Official:      true, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for reposName, expectedRepoInfo := range expectedRepoInfos { | ||||
| 		named, err := reference.WithName(reposName) | ||||
| 		if err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
| 
 | ||||
| 		repoInfo, err := ParseRepositoryInfo(named) | ||||
| 		if err != nil { | ||||
| 			t.Error(err) | ||||
| 		} else { | ||||
| 			checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName) | ||||
| 			checkEqual(t, repoInfo.RemoteName(), expectedRepoInfo.RemoteName, reposName) | ||||
| 			checkEqual(t, repoInfo.Name(), expectedRepoInfo.LocalName, reposName) | ||||
| 			checkEqual(t, repoInfo.FullName(), expectedRepoInfo.CanonicalName, reposName) | ||||
| 			checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName) | ||||
| 			checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestNewIndexInfo(t *testing.T) { | ||||
| 	testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) { | ||||
| 		for indexName, expectedIndexInfo := range expectedIndexInfos { | ||||
| 			index, err := newIndexInfo(config, indexName) | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} else { | ||||
| 				checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name") | ||||
| 				checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official") | ||||
| 				checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure") | ||||
| 				checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	config := newServiceConfig(ServiceOptions{}) | ||||
| 	noMirrors := []string{} | ||||
| 	expectedIndexInfos := map[string]*registrytypes.IndexInfo{ | ||||
| 		IndexName: { | ||||
| 			Name:     IndexName, | ||||
| 			Official: true, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"index." + IndexName: { | ||||
| 			Name:     IndexName, | ||||
| 			Official: true, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"example.com": { | ||||
| 			Name:     "example.com", | ||||
| 			Official: false, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"127.0.0.1:5000": { | ||||
| 			Name:     "127.0.0.1:5000", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 	} | ||||
| 	testIndexInfo(config, expectedIndexInfos) | ||||
| 
 | ||||
| 	publicMirrors := []string{"http://mirror1.local", "http://mirror2.local"} | ||||
| 	config = makeServiceConfig(publicMirrors, []string{"example.com"}) | ||||
| 
 | ||||
| 	expectedIndexInfos = map[string]*registrytypes.IndexInfo{ | ||||
| 		IndexName: { | ||||
| 			Name:     IndexName, | ||||
| 			Official: true, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  publicMirrors, | ||||
| 		}, | ||||
| 		"index." + IndexName: { | ||||
| 			Name:     IndexName, | ||||
| 			Official: true, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  publicMirrors, | ||||
| 		}, | ||||
| 		"example.com": { | ||||
| 			Name:     "example.com", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"example.com:5000": { | ||||
| 			Name:     "example.com:5000", | ||||
| 			Official: false, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"127.0.0.1": { | ||||
| 			Name:     "127.0.0.1", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"127.0.0.1:5000": { | ||||
| 			Name:     "127.0.0.1:5000", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"other.com": { | ||||
| 			Name:     "other.com", | ||||
| 			Official: false, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 	} | ||||
| 	testIndexInfo(config, expectedIndexInfos) | ||||
| 
 | ||||
| 	config = makeServiceConfig(nil, []string{"42.42.0.0/16"}) | ||||
| 	expectedIndexInfos = map[string]*registrytypes.IndexInfo{ | ||||
| 		"example.com": { | ||||
| 			Name:     "example.com", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"example.com:5000": { | ||||
| 			Name:     "example.com:5000", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"127.0.0.1": { | ||||
| 			Name:     "127.0.0.1", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"127.0.0.1:5000": { | ||||
| 			Name:     "127.0.0.1:5000", | ||||
| 			Official: false, | ||||
| 			Secure:   false, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 		"other.com": { | ||||
| 			Name:     "other.com", | ||||
| 			Official: false, | ||||
| 			Secure:   true, | ||||
| 			Mirrors:  noMirrors, | ||||
| 		}, | ||||
| 	} | ||||
| 	testIndexInfo(config, expectedIndexInfos) | ||||
| } | ||||
| 
 | ||||
| func TestMirrorEndpointLookup(t *testing.T) { | ||||
| 	containsMirror := func(endpoints []APIEndpoint) bool { | ||||
| 		for _, pe := range endpoints { | ||||
| 			if pe.URL.Host == "my.mirror" { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
| 	s := DefaultService{config: makeServiceConfig([]string{"my.mirror"}, nil)} | ||||
| 
 | ||||
| 	imageName, err := reference.WithName(IndexName + "/test/image") | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| 	pushAPIEndpoints, err := s.LookupPushEndpoints(imageName.Hostname()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if containsMirror(pushAPIEndpoints) { | ||||
| 		t.Fatal("Push endpoint should not contain mirror") | ||||
| 	} | ||||
| 
 | ||||
| 	pullAPIEndpoints, err := s.LookupPullEndpoints(imageName.Hostname()) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if !containsMirror(pullAPIEndpoints) { | ||||
| 		t.Fatal("Pull endpoint should contain mirror") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestPushRegistryTag(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	repoRef, err := reference.ParseNamed(REPO) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	err = r.PushRegistryTag(repoRef, imageID, "stable", makeURL("/v1/")) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestPushImageJSONIndex(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	imgData := []*ImgData{ | ||||
| 		{ | ||||
| 			ID:       "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20", | ||||
| 			Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37", | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:       "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d", | ||||
| 			Checksum: "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2", | ||||
| 		}, | ||||
| 	} | ||||
| 	repoRef, err := reference.ParseNamed(REPO) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	repoData, err := r.PushImageJSONIndex(repoRef, imgData, false, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if repoData == nil { | ||||
| 		t.Fatal("Expected RepositoryData object") | ||||
| 	} | ||||
| 	repoData, err = r.PushImageJSONIndex(repoRef, imgData, true, []string{r.indexEndpoint.String()}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if repoData == nil { | ||||
| 		t.Fatal("Expected RepositoryData object") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSearchRepositories(t *testing.T) { | ||||
| 	r := spawnTestRegistrySession(t) | ||||
| 	results, err := r.SearchRepositories("fakequery", 25) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if results == nil { | ||||
| 		t.Fatal("Expected non-nil SearchResults object") | ||||
| 	} | ||||
| 	assertEqual(t, results.NumResults, 1, "Expected 1 search results") | ||||
| 	assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query") | ||||
| 	assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") | ||||
| } | ||||
| 
 | ||||
| func TestTrustedLocation(t *testing.T) { | ||||
| 	for _, url := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} { | ||||
| 		req, _ := http.NewRequest("GET", url, nil) | ||||
| 		if trustedLocation(req) == true { | ||||
| 			t.Fatalf("'%s' shouldn't be detected as a trusted location", url) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, url := range []string{"https://docker.io", "https://test.docker.com:80"} { | ||||
| 		req, _ := http.NewRequest("GET", url, nil) | ||||
| 		if trustedLocation(req) == false { | ||||
| 			t.Fatalf("'%s' should be detected as a trusted location", url) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { | ||||
| 	for _, urls := range [][]string{ | ||||
| 		{"http://docker.io", "https://docker.com"}, | ||||
| 		{"https://foo.docker.io:7777", "http://bar.docker.com"}, | ||||
| 		{"https://foo.docker.io", "https://example.com"}, | ||||
| 	} { | ||||
| 		reqFrom, _ := http.NewRequest("GET", urls[0], nil) | ||||
| 		reqFrom.Header.Add("Content-Type", "application/json") | ||||
| 		reqFrom.Header.Add("Authorization", "super_secret") | ||||
| 		reqTo, _ := http.NewRequest("GET", urls[1], nil) | ||||
| 
 | ||||
| 		addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) | ||||
| 
 | ||||
| 		if len(reqTo.Header) != 1 { | ||||
| 			t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header)) | ||||
| 		} | ||||
| 
 | ||||
| 		if reqTo.Header.Get("Content-Type") != "application/json" { | ||||
| 			t.Fatal("'Content-Type' should be 'application/json'") | ||||
| 		} | ||||
| 
 | ||||
| 		if reqTo.Header.Get("Authorization") != "" { | ||||
| 			t.Fatal("'Authorization' should be empty") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, urls := range [][]string{ | ||||
| 		{"https://docker.io", "https://docker.com"}, | ||||
| 		{"https://foo.docker.io:7777", "https://bar.docker.com"}, | ||||
| 	} { | ||||
| 		reqFrom, _ := http.NewRequest("GET", urls[0], nil) | ||||
| 		reqFrom.Header.Add("Content-Type", "application/json") | ||||
| 		reqFrom.Header.Add("Authorization", "super_secret") | ||||
| 		reqTo, _ := http.NewRequest("GET", urls[1], nil) | ||||
| 
 | ||||
| 		addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) | ||||
| 
 | ||||
| 		if len(reqTo.Header) != 2 { | ||||
| 			t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header)) | ||||
| 		} | ||||
| 
 | ||||
| 		if reqTo.Header.Get("Content-Type") != "application/json" { | ||||
| 			t.Fatal("'Content-Type' should be 'application/json'") | ||||
| 		} | ||||
| 
 | ||||
| 		if reqTo.Header.Get("Authorization") != "super_secret" { | ||||
| 			t.Fatal("'Authorization' should be 'super_secret'") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestIsSecureIndex(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		addr               string | ||||
| 		insecureRegistries []string | ||||
| 		expected           bool | ||||
| 	}{ | ||||
| 		{IndexName, nil, true}, | ||||
| 		{"example.com", []string{}, true}, | ||||
| 		{"example.com", []string{"example.com"}, false}, | ||||
| 		{"localhost", []string{"localhost:5000"}, false}, | ||||
| 		{"localhost:5000", []string{"localhost:5000"}, false}, | ||||
| 		{"localhost", []string{"example.com"}, false}, | ||||
| 		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false}, | ||||
| 		{"localhost", nil, false}, | ||||
| 		{"localhost:5000", nil, false}, | ||||
| 		{"127.0.0.1", nil, false}, | ||||
| 		{"localhost", []string{"example.com"}, false}, | ||||
| 		{"127.0.0.1", []string{"example.com"}, false}, | ||||
| 		{"example.com", nil, true}, | ||||
| 		{"example.com", []string{"example.com"}, false}, | ||||
| 		{"127.0.0.1", []string{"example.com"}, false}, | ||||
| 		{"127.0.0.1:5000", []string{"example.com"}, false}, | ||||
| 		{"example.com:5000", []string{"42.42.0.0/16"}, false}, | ||||
| 		{"example.com", []string{"42.42.0.0/16"}, false}, | ||||
| 		{"example.com:5000", []string{"42.42.42.42/8"}, false}, | ||||
| 		{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false}, | ||||
| 		{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false}, | ||||
| 		{"invalid.domain.com", []string{"42.42.0.0/16"}, true}, | ||||
| 		{"invalid.domain.com", []string{"invalid.domain.com"}, false}, | ||||
| 		{"invalid.domain.com:5000", []string{"invalid.domain.com"}, true}, | ||||
| 		{"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		config := makeServiceConfig(nil, tt.insecureRegistries) | ||||
| 		if sec := isSecureIndex(config, tt.addr); sec != tt.expected { | ||||
| 			t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type debugTransport struct { | ||||
| 	http.RoundTripper | ||||
| 	log func(...interface{}) | ||||
| } | ||||
| 
 | ||||
| func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||||
| 	dump, err := httputil.DumpRequestOut(req, false) | ||||
| 	if err != nil { | ||||
| 		tr.log("could not dump request") | ||||
| 	} | ||||
| 	tr.log(string(dump)) | ||||
| 	resp, err := tr.RoundTripper.RoundTrip(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	dump, err = httputil.DumpResponse(resp, false) | ||||
| 	if err != nil { | ||||
| 		tr.log("could not dump response") | ||||
| 	} | ||||
| 	tr.log(string(dump)) | ||||
| 	return resp, err | ||||
| } | ||||
|  | @ -0,0 +1,260 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"golang.org/x/net/context" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/registry/client/auth" | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	"github.com/docker/engine-api/types" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// DefaultSearchLimit is the default value for maximum number of returned search results.
 | ||||
| 	DefaultSearchLimit = 25 | ||||
| ) | ||||
| 
 | ||||
| // Service is the interface defining what a registry service should implement.
 | ||||
| type Service interface { | ||||
| 	Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) | ||||
| 	LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) | ||||
| 	LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) | ||||
| 	ResolveRepository(name reference.Named) (*RepositoryInfo, error) | ||||
| 	ResolveIndex(name string) (*registrytypes.IndexInfo, error) | ||||
| 	Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) | ||||
| 	ServiceConfig() *registrytypes.ServiceConfig | ||||
| 	TLSConfig(hostname string) (*tls.Config, error) | ||||
| } | ||||
| 
 | ||||
| // DefaultService is a registry service. It tracks configuration data such as a list
 | ||||
| // of mirrors.
 | ||||
| type DefaultService struct { | ||||
| 	config *serviceConfig | ||||
| } | ||||
| 
 | ||||
| // NewService returns a new instance of DefaultService ready to be
 | ||||
| // installed into an engine.
 | ||||
| func NewService(options ServiceOptions) *DefaultService { | ||||
| 	return &DefaultService{ | ||||
| 		config: newServiceConfig(options), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ServiceConfig returns the public registry service configuration.
 | ||||
| func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig { | ||||
| 	return &s.config.ServiceConfig | ||||
| } | ||||
| 
 | ||||
| // Auth contacts the public registry with the provided credentials,
 | ||||
| // and returns OK if authentication was successful.
 | ||||
| // It can be used to verify the validity of a client's credentials.
 | ||||
| func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) { | ||||
| 	// TODO Use ctx when searching for repositories
 | ||||
| 	serverAddress := authConfig.ServerAddress | ||||
| 	if serverAddress == "" { | ||||
| 		serverAddress = IndexServer | ||||
| 	} | ||||
| 	if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { | ||||
| 		serverAddress = "https://" + serverAddress | ||||
| 	} | ||||
| 	u, err := url.Parse(serverAddress) | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("unable to parse server address: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	endpoints, err := s.LookupPushEndpoints(u.Host) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, endpoint := range endpoints { | ||||
| 		login := loginV2 | ||||
| 		if endpoint.Version == APIVersion1 { | ||||
| 			login = loginV1 | ||||
| 		} | ||||
| 
 | ||||
| 		status, token, err = login(authConfig, endpoint, userAgent) | ||||
| 		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 | ||||
| } | ||||
| 
 | ||||
| // splitReposSearchTerm breaks a search term into an index name and remote name
 | ||||
| func splitReposSearchTerm(reposName string) (string, string) { | ||||
| 	nameParts := strings.SplitN(reposName, "/", 2) | ||||
| 	var indexName, remoteName string | ||||
| 	if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && | ||||
| 		!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { | ||||
| 		// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
 | ||||
| 		// 'docker.io'
 | ||||
| 		indexName = IndexName | ||||
| 		remoteName = reposName | ||||
| 	} else { | ||||
| 		indexName = nameParts[0] | ||||
| 		remoteName = nameParts[1] | ||||
| 	} | ||||
| 	return indexName, remoteName | ||||
| } | ||||
| 
 | ||||
| // Search queries the public registry for images matching the specified
 | ||||
| // search terms, and returns the results.
 | ||||
| func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { | ||||
| 	// TODO Use ctx when searching for repositories
 | ||||
| 	if err := validateNoScheme(term); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	indexName, remoteName := splitReposSearchTerm(term) | ||||
| 
 | ||||
| 	index, err := newIndexInfo(s.config, indexName) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// *TODO: Search multiple indexes.
 | ||||
| 	endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var client *http.Client | ||||
| 	if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { | ||||
| 		creds := NewStaticCredentialStore(authConfig) | ||||
| 		scopes := []auth.Scope{ | ||||
| 			auth.RegistryScope{ | ||||
| 				Name:    "catalog", | ||||
| 				Actions: []string{"search"}, | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		modifiers := DockerHeaders(userAgent, nil) | ||||
| 		v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes) | ||||
| 		if err != nil { | ||||
| 			if fErr, ok := err.(fallbackError); ok { | ||||
| 				logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err) | ||||
| 			} else { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 		} else if foundV2 { | ||||
| 			// Copy non transport http client features
 | ||||
| 			v2Client.Timeout = endpoint.client.Timeout | ||||
| 			v2Client.CheckRedirect = endpoint.client.CheckRedirect | ||||
| 			v2Client.Jar = endpoint.client.Jar | ||||
| 
 | ||||
| 			logrus.Debugf("using v2 client for search to %s", endpoint.URL) | ||||
| 			client = v2Client | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if client == nil { | ||||
| 		client = endpoint.client | ||||
| 		if err := authorizeClient(client, authConfig, endpoint); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	r := newSession(client, authConfig, endpoint) | ||||
| 
 | ||||
| 	if index.Official { | ||||
| 		localName := remoteName | ||||
| 		if strings.HasPrefix(localName, "library/") { | ||||
| 			// If pull "library/foo", it's stored locally under "foo"
 | ||||
| 			localName = strings.SplitN(localName, "/", 2)[1] | ||||
| 		} | ||||
| 
 | ||||
| 		return r.SearchRepositories(localName, limit) | ||||
| 	} | ||||
| 	return r.SearchRepositories(remoteName, limit) | ||||
| } | ||||
| 
 | ||||
| // ResolveRepository splits a repository name into its components
 | ||||
| // and configuration of the associated registry.
 | ||||
| func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { | ||||
| 	return newRepositoryInfo(s.config, name) | ||||
| } | ||||
| 
 | ||||
| // ResolveIndex takes indexName and returns index info
 | ||||
| func (s *DefaultService) ResolveIndex(name string) (*registrytypes.IndexInfo, error) { | ||||
| 	return newIndexInfo(s.config, name) | ||||
| } | ||||
| 
 | ||||
| // APIEndpoint represents a remote API endpoint
 | ||||
| type APIEndpoint struct { | ||||
| 	Mirror       bool | ||||
| 	URL          *url.URL | ||||
| 	Version      APIVersion | ||||
| 	Official     bool | ||||
| 	TrimHostname bool | ||||
| 	TLSConfig    *tls.Config | ||||
| } | ||||
| 
 | ||||
| // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
 | ||||
| func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) { | ||||
| 	return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders) | ||||
| } | ||||
| 
 | ||||
| // TLSConfig constructs a client TLS configuration based on server defaults
 | ||||
| func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) { | ||||
| 	return newTLSConfig(hostname, isSecureIndex(s.config, hostname)) | ||||
| } | ||||
| 
 | ||||
| func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) { | ||||
| 	return s.TLSConfig(mirrorURL.Host) | ||||
| } | ||||
| 
 | ||||
| // LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference.
 | ||||
| // It gives preference to v2 endpoints over v1, mirrors over the actual
 | ||||
| // registry, and HTTPS over plain HTTP.
 | ||||
| func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { | ||||
| 	return s.lookupEndpoints(hostname) | ||||
| } | ||||
| 
 | ||||
| // LookupPushEndpoints creates a 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.
 | ||||
| // Mirrors are not included.
 | ||||
| func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { | ||||
| 	allEndpoints, err := s.lookupEndpoints(hostname) | ||||
| 	if err == nil { | ||||
| 		for _, endpoint := range allEndpoints { | ||||
| 			if !endpoint.Mirror { | ||||
| 				endpoints = append(endpoints, endpoint) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return endpoints, err | ||||
| } | ||||
| 
 | ||||
| func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) { | ||||
| 	endpoints, err = s.lookupV2Endpoints(hostname) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if s.config.V2Only { | ||||
| 		return endpoints, nil | ||||
| 	} | ||||
| 
 | ||||
| 	legacyEndpoints, err := s.lookupV1Endpoints(hostname) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	endpoints = append(endpoints, legacyEndpoints...) | ||||
| 
 | ||||
| 	return endpoints, nil | ||||
| } | ||||
|  | @ -0,0 +1,53 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/docker/go-connections/tlsconfig" | ||||
| ) | ||||
| 
 | ||||
| func (s *DefaultService) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) { | ||||
| 	var cfg = tlsconfig.ServerDefault | ||||
| 	tlsConfig := &cfg | ||||
| 	if hostname == DefaultNamespace { | ||||
| 		endpoints = append(endpoints, APIEndpoint{ | ||||
| 			URL:          DefaultV1Registry, | ||||
| 			Version:      APIVersion1, | ||||
| 			Official:     true, | ||||
| 			TrimHostname: true, | ||||
| 			TLSConfig:    tlsConfig, | ||||
| 		}) | ||||
| 		return endpoints, nil | ||||
| 	} | ||||
| 
 | ||||
| 	tlsConfig, err = s.TLSConfig(hostname) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	endpoints = []APIEndpoint{ | ||||
| 		{ | ||||
| 			URL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   hostname, | ||||
| 			}, | ||||
| 			Version:      APIVersion1, | ||||
| 			TrimHostname: true, | ||||
| 			TLSConfig:    tlsConfig, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	if tlsConfig.InsecureSkipVerify { | ||||
| 		endpoints = append(endpoints, APIEndpoint{ // or this
 | ||||
| 			URL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   hostname, | ||||
| 			}, | ||||
| 			Version:      APIVersion1, | ||||
| 			TrimHostname: true, | ||||
| 			// used to check if supposed to be secure via InsecureSkipVerify
 | ||||
| 			TLSConfig: tlsConfig, | ||||
| 		}) | ||||
| 	} | ||||
| 	return endpoints, nil | ||||
| } | ||||
|  | @ -0,0 +1,79 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/docker/go-connections/tlsconfig" | ||||
| ) | ||||
| 
 | ||||
| func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { | ||||
| 	var cfg = tlsconfig.ServerDefault | ||||
| 	tlsConfig := &cfg | ||||
| 	if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host { | ||||
| 		// v2 mirrors
 | ||||
| 		for _, mirror := range s.config.Mirrors { | ||||
| 			if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { | ||||
| 				mirror = "https://" + mirror | ||||
| 			} | ||||
| 			mirrorURL, err := url.Parse(mirror) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			endpoints = append(endpoints, APIEndpoint{ | ||||
| 				URL: mirrorURL, | ||||
| 				// guess mirrors are v2
 | ||||
| 				Version:      APIVersion2, | ||||
| 				Mirror:       true, | ||||
| 				TrimHostname: true, | ||||
| 				TLSConfig:    mirrorTLSConfig, | ||||
| 			}) | ||||
| 		} | ||||
| 		// v2 registry
 | ||||
| 		endpoints = append(endpoints, APIEndpoint{ | ||||
| 			URL:          DefaultV2Registry, | ||||
| 			Version:      APIVersion2, | ||||
| 			Official:     true, | ||||
| 			TrimHostname: true, | ||||
| 			TLSConfig:    tlsConfig, | ||||
| 		}) | ||||
| 
 | ||||
| 		return endpoints, nil | ||||
| 	} | ||||
| 
 | ||||
| 	tlsConfig, err = s.TLSConfig(hostname) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	endpoints = []APIEndpoint{ | ||||
| 		{ | ||||
| 			URL: &url.URL{ | ||||
| 				Scheme: "https", | ||||
| 				Host:   hostname, | ||||
| 			}, | ||||
| 			Version:      APIVersion2, | ||||
| 			TrimHostname: true, | ||||
| 			TLSConfig:    tlsConfig, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	if tlsConfig.InsecureSkipVerify { | ||||
| 		endpoints = append(endpoints, APIEndpoint{ | ||||
| 			URL: &url.URL{ | ||||
| 				Scheme: "http", | ||||
| 				Host:   hostname, | ||||
| 			}, | ||||
| 			Version:      APIVersion2, | ||||
| 			TrimHostname: true, | ||||
| 			// used to check if supposed to be secure via InsecureSkipVerify
 | ||||
| 			TLSConfig: tlsConfig, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return endpoints, nil | ||||
| } | ||||
|  | @ -0,0 +1,783 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/sha256" | ||||
| 	"errors" | ||||
| 	"sync" | ||||
| 	// this is required for some certificates
 | ||||
| 	_ "crypto/sha512" | ||||
| 	"encoding/hex" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/cookiejar" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/registry/api/errcode" | ||||
| 	"github.com/docker/docker/pkg/httputils" | ||||
| 	"github.com/docker/docker/pkg/ioutils" | ||||
| 	"github.com/docker/docker/pkg/stringid" | ||||
| 	"github.com/docker/docker/pkg/tarsum" | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	"github.com/docker/engine-api/types" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	// ErrRepoNotFound is returned if the repository didn't exist on the
 | ||||
| 	// remote side
 | ||||
| 	ErrRepoNotFound = errors.New("Repository not found") | ||||
| ) | ||||
| 
 | ||||
| // A Session is used to communicate with a V1 registry
 | ||||
| type Session struct { | ||||
| 	indexEndpoint *V1Endpoint | ||||
| 	client        *http.Client | ||||
| 	// TODO(tiborvass): remove authConfig
 | ||||
| 	authConfig *types.AuthConfig | ||||
| 	id         string | ||||
| } | ||||
| 
 | ||||
| type authTransport struct { | ||||
| 	http.RoundTripper | ||||
| 	*types.AuthConfig | ||||
| 
 | ||||
| 	alwaysSetBasicAuth bool | ||||
| 	token              []string | ||||
| 
 | ||||
| 	mu     sync.Mutex                      // guards modReq
 | ||||
| 	modReq map[*http.Request]*http.Request // original -> modified
 | ||||
| } | ||||
| 
 | ||||
| // AuthTransport handles the auth layer when communicating with a v1 registry (private or official)
 | ||||
| //
 | ||||
| // For private v1 registries, set alwaysSetBasicAuth to true.
 | ||||
| //
 | ||||
| // For the official v1 registry, if there isn't already an Authorization header in the request,
 | ||||
| // but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
 | ||||
| // After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
 | ||||
| // a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
 | ||||
| // requests.
 | ||||
| //
 | ||||
| // If the server sends a token without the client having requested it, it is ignored.
 | ||||
| //
 | ||||
| // This RoundTripper also has a CancelRequest method important for correct timeout handling.
 | ||||
| func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper { | ||||
| 	if base == nil { | ||||
| 		base = http.DefaultTransport | ||||
| 	} | ||||
| 	return &authTransport{ | ||||
| 		RoundTripper:       base, | ||||
| 		AuthConfig:         authConfig, | ||||
| 		alwaysSetBasicAuth: alwaysSetBasicAuth, | ||||
| 		modReq:             make(map[*http.Request]*http.Request), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // cloneRequest returns a clone of the provided *http.Request.
 | ||||
| // The clone is a shallow copy of the struct and its Header map.
 | ||||
| func cloneRequest(r *http.Request) *http.Request { | ||||
| 	// shallow copy of the struct
 | ||||
| 	r2 := new(http.Request) | ||||
| 	*r2 = *r | ||||
| 	// deep copy of the Header
 | ||||
| 	r2.Header = make(http.Header, len(r.Header)) | ||||
| 	for k, s := range r.Header { | ||||
| 		r2.Header[k] = append([]string(nil), s...) | ||||
| 	} | ||||
| 
 | ||||
| 	return r2 | ||||
| } | ||||
| 
 | ||||
| // RoundTrip changes an HTTP request's headers to add the necessary
 | ||||
| // authentication-related headers
 | ||||
| func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { | ||||
| 	// Authorization should not be set on 302 redirect for untrusted locations.
 | ||||
| 	// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
 | ||||
| 	// As the authorization logic is currently implemented in RoundTrip,
 | ||||
| 	// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
 | ||||
| 	// This is safe as Docker doesn't set Referrer in other scenarios.
 | ||||
| 	if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { | ||||
| 		return tr.RoundTripper.RoundTrip(orig) | ||||
| 	} | ||||
| 
 | ||||
| 	req := cloneRequest(orig) | ||||
| 	tr.mu.Lock() | ||||
| 	tr.modReq[orig] = req | ||||
| 	tr.mu.Unlock() | ||||
| 
 | ||||
| 	if tr.alwaysSetBasicAuth { | ||||
| 		if tr.AuthConfig == nil { | ||||
| 			return nil, errors.New("unexpected error: empty auth config") | ||||
| 		} | ||||
| 		req.SetBasicAuth(tr.Username, tr.Password) | ||||
| 		return tr.RoundTripper.RoundTrip(req) | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't override
 | ||||
| 	if req.Header.Get("Authorization") == "" { | ||||
| 		if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 { | ||||
| 			req.SetBasicAuth(tr.Username, tr.Password) | ||||
| 		} else if len(tr.token) > 0 { | ||||
| 			req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) | ||||
| 		} | ||||
| 	} | ||||
| 	resp, err := tr.RoundTripper.RoundTrip(req) | ||||
| 	if err != nil { | ||||
| 		delete(tr.modReq, orig) | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(resp.Header["X-Docker-Token"]) > 0 { | ||||
| 		tr.token = resp.Header["X-Docker-Token"] | ||||
| 	} | ||||
| 	resp.Body = &ioutils.OnEOFReader{ | ||||
| 		Rc: resp.Body, | ||||
| 		Fn: func() { | ||||
| 			tr.mu.Lock() | ||||
| 			delete(tr.modReq, orig) | ||||
| 			tr.mu.Unlock() | ||||
| 		}, | ||||
| 	} | ||||
| 	return resp, nil | ||||
| } | ||||
| 
 | ||||
| // CancelRequest cancels an in-flight request by closing its connection.
 | ||||
| func (tr *authTransport) CancelRequest(req *http.Request) { | ||||
| 	type canceler interface { | ||||
| 		CancelRequest(*http.Request) | ||||
| 	} | ||||
| 	if cr, ok := tr.RoundTripper.(canceler); ok { | ||||
| 		tr.mu.Lock() | ||||
| 		modReq := tr.modReq[req] | ||||
| 		delete(tr.modReq, req) | ||||
| 		tr.mu.Unlock() | ||||
| 		cr.CancelRequest(modReq) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error { | ||||
| 	var alwaysSetBasicAuth bool | ||||
| 
 | ||||
| 	// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
 | ||||
| 	// alongside all our requests.
 | ||||
| 	if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { | ||||
| 		info, err := endpoint.Ping() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if info.Standalone && authConfig != nil { | ||||
| 			logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String()) | ||||
| 			alwaysSetBasicAuth = true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Annotate the transport unconditionally so that v2 can
 | ||||
| 	// properly fallback on v1 when an image is not found.
 | ||||
| 	client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) | ||||
| 
 | ||||
| 	jar, err := cookiejar.New(nil) | ||||
| 	if err != nil { | ||||
| 		return errors.New("cookiejar.New is not supposed to return an error") | ||||
| 	} | ||||
| 	client.Jar = jar | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session { | ||||
| 	return &Session{ | ||||
| 		authConfig:    authConfig, | ||||
| 		client:        client, | ||||
| 		indexEndpoint: endpoint, | ||||
| 		id:            stringid.GenerateRandomID(), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewSession creates a new session
 | ||||
| // TODO(tiborvass): remove authConfig param once registry client v2 is vendored
 | ||||
| func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) { | ||||
| 	if err := authorizeClient(client, authConfig, endpoint); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return newSession(client, authConfig, endpoint), nil | ||||
| } | ||||
| 
 | ||||
| // ID returns this registry session's ID.
 | ||||
| func (r *Session) ID() string { | ||||
| 	return r.id | ||||
| } | ||||
| 
 | ||||
| // GetRemoteHistory retrieves the history of a given image from the registry.
 | ||||
| // It returns a list of the parent's JSON files (including the requested image).
 | ||||
| func (r *Session) GetRemoteHistory(imgID, registry string) ([]string, error) { | ||||
| 	res, err := r.client.Get(registry + "images/" + imgID + "/ancestry") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if res.StatusCode != 200 { | ||||
| 		if res.StatusCode == 401 { | ||||
| 			return nil, errcode.ErrorCodeUnauthorized.WithArgs() | ||||
| 		} | ||||
| 		return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res) | ||||
| 	} | ||||
| 
 | ||||
| 	var history []string | ||||
| 	if err := json.NewDecoder(res.Body).Decode(&history); err != nil { | ||||
| 		return nil, fmt.Errorf("Error while reading the http response: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	logrus.Debugf("Ancestry: %v", history) | ||||
| 	return history, nil | ||||
| } | ||||
| 
 | ||||
| // LookupRemoteImage checks if an image exists in the registry
 | ||||
| func (r *Session) LookupRemoteImage(imgID, registry string) error { | ||||
| 	res, err := r.client.Get(registry + "images/" + imgID + "/json") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	res.Body.Close() | ||||
| 	if res.StatusCode != 200 { | ||||
| 		return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // GetRemoteImageJSON retrieves an image's JSON metadata from the registry.
 | ||||
| func (r *Session) GetRemoteImageJSON(imgID, registry string) ([]byte, int64, error) { | ||||
| 	res, err := r.client.Get(registry + "images/" + imgID + "/json") | ||||
| 	if err != nil { | ||||
| 		return nil, -1, fmt.Errorf("Failed to download json: %s", err) | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if res.StatusCode != 200 { | ||||
| 		return nil, -1, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res) | ||||
| 	} | ||||
| 	// if the size header is not present, then set it to '-1'
 | ||||
| 	imageSize := int64(-1) | ||||
| 	if hdr := res.Header.Get("X-Docker-Size"); hdr != "" { | ||||
| 		imageSize, err = strconv.ParseInt(hdr, 10, 64) | ||||
| 		if err != nil { | ||||
| 			return nil, -1, err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	jsonString, err := ioutil.ReadAll(res.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, -1, fmt.Errorf("Failed to parse downloaded json: %v (%s)", err, jsonString) | ||||
| 	} | ||||
| 	return jsonString, imageSize, nil | ||||
| } | ||||
| 
 | ||||
| // GetRemoteImageLayer retrieves an image layer from the registry
 | ||||
| func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io.ReadCloser, error) { | ||||
| 	var ( | ||||
| 		statusCode = 0 | ||||
| 		res        *http.Response | ||||
| 		err        error | ||||
| 		imageURL   = fmt.Sprintf("%simages/%s/layer", registry, imgID) | ||||
| 	) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", imageURL, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Error while getting from the server: %v", err) | ||||
| 	} | ||||
| 	statusCode = 0 | ||||
| 	res, err = r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		logrus.Debugf("Error contacting registry %s: %v", registry, err) | ||||
| 		// the only case err != nil && res != nil is https://golang.org/src/net/http/client.go#L515
 | ||||
| 		if res != nil { | ||||
| 			if res.Body != nil { | ||||
| 				res.Body.Close() | ||||
| 			} | ||||
| 			statusCode = res.StatusCode | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", | ||||
| 			statusCode, imgID) | ||||
| 	} | ||||
| 
 | ||||
| 	if res.StatusCode != 200 { | ||||
| 		res.Body.Close() | ||||
| 		return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)", | ||||
| 			res.StatusCode, imgID) | ||||
| 	} | ||||
| 
 | ||||
| 	if res.Header.Get("Accept-Ranges") == "bytes" && imgSize > 0 { | ||||
| 		logrus.Debug("server supports resume") | ||||
| 		return httputils.ResumableRequestReaderWithInitialResponse(r.client, req, 5, imgSize, res), nil | ||||
| 	} | ||||
| 	logrus.Debug("server doesn't support resume") | ||||
| 	return res.Body, nil | ||||
| } | ||||
| 
 | ||||
| // GetRemoteTag retrieves the tag named in the askedTag argument from the given
 | ||||
| // repository. It queries each of the registries supplied in the registries
 | ||||
| // argument, and returns data from the first one that answers the query
 | ||||
| // successfully.
 | ||||
| func (r *Session) GetRemoteTag(registries []string, repositoryRef reference.Named, askedTag string) (string, error) { | ||||
| 	repository := repositoryRef.RemoteName() | ||||
| 
 | ||||
| 	if strings.Count(repository, "/") == 0 { | ||||
| 		// This will be removed once the registry supports auto-resolution on
 | ||||
| 		// the "library" namespace
 | ||||
| 		repository = "library/" + repository | ||||
| 	} | ||||
| 	for _, host := range registries { | ||||
| 		endpoint := fmt.Sprintf("%srepositories/%s/tags/%s", host, repository, askedTag) | ||||
| 		res, err := r.client.Get(endpoint) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 
 | ||||
| 		logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint) | ||||
| 		defer res.Body.Close() | ||||
| 
 | ||||
| 		if res.StatusCode == 404 { | ||||
| 			return "", ErrRepoNotFound | ||||
| 		} | ||||
| 		if res.StatusCode != 200 { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		var tagID string | ||||
| 		if err := json.NewDecoder(res.Body).Decode(&tagID); err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		return tagID, nil | ||||
| 	} | ||||
| 	return "", fmt.Errorf("Could not reach any registry endpoint") | ||||
| } | ||||
| 
 | ||||
| // GetRemoteTags retrieves all tags from the given repository. It queries each
 | ||||
| // of the registries supplied in the registries argument, and returns data from
 | ||||
| // the first one that answers the query successfully. It returns a map with
 | ||||
| // tag names as the keys and image IDs as the values.
 | ||||
| func (r *Session) GetRemoteTags(registries []string, repositoryRef reference.Named) (map[string]string, error) { | ||||
| 	repository := repositoryRef.RemoteName() | ||||
| 
 | ||||
| 	if strings.Count(repository, "/") == 0 { | ||||
| 		// This will be removed once the registry supports auto-resolution on
 | ||||
| 		// the "library" namespace
 | ||||
| 		repository = "library/" + repository | ||||
| 	} | ||||
| 	for _, host := range registries { | ||||
| 		endpoint := fmt.Sprintf("%srepositories/%s/tags", host, repository) | ||||
| 		res, err := r.client.Get(endpoint) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint) | ||||
| 		defer res.Body.Close() | ||||
| 
 | ||||
| 		if res.StatusCode == 404 { | ||||
| 			return nil, ErrRepoNotFound | ||||
| 		} | ||||
| 		if res.StatusCode != 200 { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		result := make(map[string]string) | ||||
| 		if err := json.NewDecoder(res.Body).Decode(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		return result, nil | ||||
| 	} | ||||
| 	return nil, fmt.Errorf("Could not reach any registry endpoint") | ||||
| } | ||||
| 
 | ||||
| func buildEndpointsList(headers []string, indexEp string) ([]string, error) { | ||||
| 	var endpoints []string | ||||
| 	parsedURL, err := url.Parse(indexEp) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var urlScheme = parsedURL.Scheme | ||||
| 	// The registry's URL scheme has to match the Index'
 | ||||
| 	for _, ep := range headers { | ||||
| 		epList := strings.Split(ep, ",") | ||||
| 		for _, epListElement := range epList { | ||||
| 			endpoints = append( | ||||
| 				endpoints, | ||||
| 				fmt.Sprintf("%s://%s/v1/", urlScheme, strings.TrimSpace(epListElement))) | ||||
| 		} | ||||
| 	} | ||||
| 	return endpoints, nil | ||||
| } | ||||
| 
 | ||||
| // GetRepositoryData returns lists of images and endpoints for the repository
 | ||||
| func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) { | ||||
| 	repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName()) | ||||
| 
 | ||||
| 	logrus.Debugf("[registry] Calling GET %s", repositoryTarget) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", repositoryTarget, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests
 | ||||
| 	req.Header.Set("X-Docker-Token", "true") | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		// check if the error is because of i/o timeout
 | ||||
| 		// and return a non-obtuse error message for users
 | ||||
| 		// "Get https://index.docker.io/v1/repositories/library/busybox/images: i/o timeout"
 | ||||
| 		// was a top search on the docker user forum
 | ||||
| 		if isTimeout(err) { | ||||
| 			return nil, fmt.Errorf("Network timed out while trying to connect to %s. You may want to check your internet connection or if you are behind a proxy.", repositoryTarget) | ||||
| 		} | ||||
| 		return nil, fmt.Errorf("Error while pulling image: %v", err) | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if res.StatusCode == 401 { | ||||
| 		return nil, errcode.ErrorCodeUnauthorized.WithArgs() | ||||
| 	} | ||||
| 	// TODO: Right now we're ignoring checksums in the response body.
 | ||||
| 	// In the future, we need to use them to check image validity.
 | ||||
| 	if res.StatusCode == 404 { | ||||
| 		return nil, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) | ||||
| 	} else if res.StatusCode != 200 { | ||||
| 		errBody, err := ioutil.ReadAll(res.Body) | ||||
| 		if err != nil { | ||||
| 			logrus.Debugf("Error reading response body: %s", err) | ||||
| 		} | ||||
| 		return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, name.RemoteName(), errBody), res) | ||||
| 	} | ||||
| 
 | ||||
| 	var endpoints []string | ||||
| 	if res.Header.Get("X-Docker-Endpoints") != "" { | ||||
| 		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Assume the endpoint is on the same host
 | ||||
| 		endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host)) | ||||
| 	} | ||||
| 
 | ||||
| 	remoteChecksums := []*ImgData{} | ||||
| 	if err := json.NewDecoder(res.Body).Decode(&remoteChecksums); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Forge a better object from the retrieved data
 | ||||
| 	imgsData := make(map[string]*ImgData, len(remoteChecksums)) | ||||
| 	for _, elem := range remoteChecksums { | ||||
| 		imgsData[elem.ID] = elem | ||||
| 	} | ||||
| 
 | ||||
| 	return &RepositoryData{ | ||||
| 		ImgList:   imgsData, | ||||
| 		Endpoints: endpoints, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // PushImageChecksumRegistry uploads checksums for an image
 | ||||
| func (r *Session) PushImageChecksumRegistry(imgData *ImgData, registry string) error { | ||||
| 	u := registry + "images/" + imgData.ID + "/checksum" | ||||
| 
 | ||||
| 	logrus.Debugf("[registry] Calling PUT %s", u) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("PUT", u, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req.Header.Set("X-Docker-Checksum", imgData.Checksum) | ||||
| 	req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload) | ||||
| 
 | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Failed to upload metadata: %v", err) | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if len(res.Cookies()) > 0 { | ||||
| 		r.client.Jar.SetCookies(req.URL, res.Cookies()) | ||||
| 	} | ||||
| 	if res.StatusCode != 200 { | ||||
| 		errBody, err := ioutil.ReadAll(res.Body) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err) | ||||
| 		} | ||||
| 		var jsonBody map[string]string | ||||
| 		if err := json.Unmarshal(errBody, &jsonBody); err != nil { | ||||
| 			errBody = []byte(err.Error()) | ||||
| 		} else if jsonBody["error"] == "Image already exists" { | ||||
| 			return ErrAlreadyExists | ||||
| 		} | ||||
| 		return fmt.Errorf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // PushImageJSONRegistry pushes JSON metadata for a local image to the registry
 | ||||
| func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string) error { | ||||
| 
 | ||||
| 	u := registry + "images/" + imgData.ID + "/json" | ||||
| 
 | ||||
| 	logrus.Debugf("[registry] Calling PUT %s", u) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("PUT", u, bytes.NewReader(jsonRaw)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req.Header.Add("Content-type", "application/json") | ||||
| 
 | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Failed to upload metadata: %s", err) | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") { | ||||
| 		return httputils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res) | ||||
| 	} | ||||
| 	if res.StatusCode != 200 { | ||||
| 		errBody, err := ioutil.ReadAll(res.Body) | ||||
| 		if err != nil { | ||||
| 			return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) | ||||
| 		} | ||||
| 		var jsonBody map[string]string | ||||
| 		if err := json.Unmarshal(errBody, &jsonBody); err != nil { | ||||
| 			errBody = []byte(err.Error()) | ||||
| 		} else if jsonBody["error"] == "Image already exists" { | ||||
| 			return ErrAlreadyExists | ||||
| 		} | ||||
| 		return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // PushImageLayerRegistry sends the checksum of an image layer to the registry
 | ||||
| func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, jsonRaw []byte) (checksum string, checksumPayload string, err error) { | ||||
| 	u := registry + "images/" + imgID + "/layer" | ||||
| 
 | ||||
| 	logrus.Debugf("[registry] Calling PUT %s", u) | ||||
| 
 | ||||
| 	tarsumLayer, err := tarsum.NewTarSum(layer, false, tarsum.Version0) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	h := sha256.New() | ||||
| 	h.Write(jsonRaw) | ||||
| 	h.Write([]byte{'\n'}) | ||||
| 	checksumLayer := io.TeeReader(tarsumLayer, h) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("PUT", u, checksumLayer) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| 	req.Header.Add("Content-Type", "application/octet-stream") | ||||
| 	req.ContentLength = -1 | ||||
| 	req.TransferEncoding = []string{"chunked"} | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return "", "", fmt.Errorf("Failed to upload layer: %v", err) | ||||
| 	} | ||||
| 	if rc, ok := layer.(io.Closer); ok { | ||||
| 		if err := rc.Close(); err != nil { | ||||
| 			return "", "", err | ||||
| 		} | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 
 | ||||
| 	if res.StatusCode != 200 { | ||||
| 		errBody, err := ioutil.ReadAll(res.Body) | ||||
| 		if err != nil { | ||||
| 			return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) | ||||
| 		} | ||||
| 		return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res) | ||||
| 	} | ||||
| 
 | ||||
| 	checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil)) | ||||
| 	return tarsumLayer.Sum(jsonRaw), checksumPayload, nil | ||||
| } | ||||
| 
 | ||||
| // PushRegistryTag pushes a tag on the registry.
 | ||||
| // Remote has the format '<user>/<repo>
 | ||||
| func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registry string) error { | ||||
| 	// "jsonify" the string
 | ||||
| 	revision = "\"" + revision + "\"" | ||||
| 	path := fmt.Sprintf("repositories/%s/tags/%s", remote.RemoteName(), tag) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("PUT", registry+path, strings.NewReader(revision)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req.Header.Add("Content-type", "application/json") | ||||
| 	req.ContentLength = int64(len(revision)) | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	res.Body.Close() | ||||
| 	if res.StatusCode != 200 && res.StatusCode != 201 { | ||||
| 		return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote.RemoteName()), res) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // PushImageJSONIndex uploads an image list to the repository
 | ||||
| func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) { | ||||
| 	cleanImgList := []*ImgData{} | ||||
| 	if validate { | ||||
| 		for _, elem := range imgList { | ||||
| 			if elem.Checksum != "" { | ||||
| 				cleanImgList = append(cleanImgList, elem) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		cleanImgList = imgList | ||||
| 	} | ||||
| 
 | ||||
| 	imgListJSON, err := json.Marshal(cleanImgList) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var suffix string | ||||
| 	if validate { | ||||
| 		suffix = "images" | ||||
| 	} | ||||
| 	u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix) | ||||
| 	logrus.Debugf("[registry] PUT %s", u) | ||||
| 	logrus.Debugf("Image list pushed to index:\n%s", imgListJSON) | ||||
| 	headers := map[string][]string{ | ||||
| 		"Content-type": {"application/json"}, | ||||
| 		// this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests
 | ||||
| 		"X-Docker-Token": {"true"}, | ||||
| 	} | ||||
| 	if validate { | ||||
| 		headers["X-Docker-Endpoints"] = regs | ||||
| 	} | ||||
| 
 | ||||
| 	// Redirect if necessary
 | ||||
| 	var res *http.Response | ||||
| 	for { | ||||
| 		if res, err = r.putImageRequest(u, headers, imgListJSON); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if !shouldRedirect(res) { | ||||
| 			break | ||||
| 		} | ||||
| 		res.Body.Close() | ||||
| 		u = res.Header.Get("Location") | ||||
| 		logrus.Debugf("Redirected to %s", u) | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 
 | ||||
| 	if res.StatusCode == 401 { | ||||
| 		return nil, errcode.ErrorCodeUnauthorized.WithArgs() | ||||
| 	} | ||||
| 
 | ||||
| 	var tokens, endpoints []string | ||||
| 	if !validate { | ||||
| 		if res.StatusCode != 200 && res.StatusCode != 201 { | ||||
| 			errBody, err := ioutil.ReadAll(res.Body) | ||||
| 			if err != nil { | ||||
| 				logrus.Debugf("Error reading response body: %s", err) | ||||
| 			} | ||||
| 			return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote.RemoteName(), errBody), res) | ||||
| 		} | ||||
| 		tokens = res.Header["X-Docker-Token"] | ||||
| 		logrus.Debugf("Auth token: %v", tokens) | ||||
| 
 | ||||
| 		if res.Header.Get("X-Docker-Endpoints") == "" { | ||||
| 			return nil, fmt.Errorf("Index response didn't contain any endpoints") | ||||
| 		} | ||||
| 		endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		if res.StatusCode != 204 { | ||||
| 			errBody, err := ioutil.ReadAll(res.Body) | ||||
| 			if err != nil { | ||||
| 				logrus.Debugf("Error reading response body: %s", err) | ||||
| 			} | ||||
| 			return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote.RemoteName(), errBody), res) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return &RepositoryData{ | ||||
| 		Endpoints: endpoints, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (r *Session) putImageRequest(u string, headers map[string][]string, body []byte) (*http.Response, error) { | ||||
| 	req, err := http.NewRequest("PUT", u, bytes.NewReader(body)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.ContentLength = int64(len(body)) | ||||
| 	for k, v := range headers { | ||||
| 		req.Header[k] = v | ||||
| 	} | ||||
| 	response, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return response, nil | ||||
| } | ||||
| 
 | ||||
| func shouldRedirect(response *http.Response) bool { | ||||
| 	return response.StatusCode >= 300 && response.StatusCode < 400 | ||||
| } | ||||
| 
 | ||||
| // SearchRepositories performs a search against the remote repository
 | ||||
| func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) { | ||||
| 	if limit < 1 || limit > 100 { | ||||
| 		return nil, fmt.Errorf("Limit %d is outside the range of [1, 100]", limit) | ||||
| 	} | ||||
| 	logrus.Debugf("Index server: %s", r.indexEndpoint) | ||||
| 	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", u, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Error while getting from the server: %v", err) | ||||
| 	} | ||||
| 	// Have the AuthTransport send authentication, when logged in.
 | ||||
| 	req.Header.Set("X-Docker-Token", "true") | ||||
| 	res, err := r.client.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer res.Body.Close() | ||||
| 	if res.StatusCode != 200 { | ||||
| 		return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) | ||||
| 	} | ||||
| 	result := new(registrytypes.SearchResults) | ||||
| 	return result, json.NewDecoder(res.Body).Decode(result) | ||||
| } | ||||
| 
 | ||||
| // GetAuthConfig returns the authentication settings for a session
 | ||||
| // TODO(tiborvass): remove this once registry client v2 is vendored
 | ||||
| func (r *Session) GetAuthConfig(withPasswd bool) *types.AuthConfig { | ||||
| 	password := "" | ||||
| 	if withPasswd { | ||||
| 		password = r.authConfig.Password | ||||
| 	} | ||||
| 	return &types.AuthConfig{ | ||||
| 		Username: r.authConfig.Username, | ||||
| 		Password: password, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func isTimeout(err error) bool { | ||||
| 	type timeout interface { | ||||
| 		Timeout() bool | ||||
| 	} | ||||
| 	e := err | ||||
| 	switch urlErr := err.(type) { | ||||
| 	case *url.Error: | ||||
| 		e = urlErr.Err | ||||
| 	} | ||||
| 	t, ok := e.(timeout) | ||||
| 	return ok && t.Timeout() | ||||
| } | ||||
|  | @ -0,0 +1,70 @@ | |||
| package registry | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/docker/docker/reference" | ||||
| 	registrytypes "github.com/docker/engine-api/types/registry" | ||||
| ) | ||||
| 
 | ||||
| // RepositoryData tracks the image list, list of endpoints, and list of tokens
 | ||||
| // for a repository
 | ||||
| type RepositoryData struct { | ||||
| 	// ImgList is a list of images in the repository
 | ||||
| 	ImgList map[string]*ImgData | ||||
| 	// Endpoints is a list of endpoints returned in X-Docker-Endpoints
 | ||||
| 	Endpoints []string | ||||
| 	// Tokens is currently unused (remove it?)
 | ||||
| 	Tokens []string | ||||
| } | ||||
| 
 | ||||
| // ImgData is used to transfer image checksums to and from the registry
 | ||||
| type ImgData struct { | ||||
| 	// ID is an opaque string that identifies the image
 | ||||
| 	ID              string `json:"id"` | ||||
| 	Checksum        string `json:"checksum,omitempty"` | ||||
| 	ChecksumPayload string `json:"-"` | ||||
| 	Tag             string `json:",omitempty"` | ||||
| } | ||||
| 
 | ||||
| // PingResult contains the information returned when pinging a registry. It
 | ||||
| // indicates the registry's version and whether the registry claims to be a
 | ||||
| // standalone registry.
 | ||||
| type PingResult struct { | ||||
| 	// Version is the registry version supplied by the registry in an HTTP
 | ||||
| 	// header
 | ||||
| 	Version string `json:"version"` | ||||
| 	// Standalone is set to true if the registry indicates it is a
 | ||||
| 	// standalone registry in the X-Docker-Registry-Standalone
 | ||||
| 	// header
 | ||||
| 	Standalone bool `json:"standalone"` | ||||
| } | ||||
| 
 | ||||
| // APIVersion is an integral representation of an API version (presently
 | ||||
| // either 1 or 2)
 | ||||
| type APIVersion int | ||||
| 
 | ||||
| func (av APIVersion) String() string { | ||||
| 	return apiVersions[av] | ||||
| } | ||||
| 
 | ||||
| // API Version identifiers.
 | ||||
| const ( | ||||
| 	_                      = iota | ||||
| 	APIVersion1 APIVersion = iota | ||||
| 	APIVersion2 | ||||
| ) | ||||
| 
 | ||||
| var apiVersions = map[APIVersion]string{ | ||||
| 	APIVersion1: "v1", | ||||
| 	APIVersion2: "v2", | ||||
| } | ||||
| 
 | ||||
| // RepositoryInfo describes a repository
 | ||||
| type RepositoryInfo struct { | ||||
| 	reference.Named | ||||
| 	// Index points to registry information
 | ||||
| 	Index *registrytypes.IndexInfo | ||||
| 	// Official indicates whether the repository is considered official.
 | ||||
| 	// If the registry is official, and the normalized name does not
 | ||||
| 	// contain a '/' (e.g. "foo"), then it is considered an official repo.
 | ||||
| 	Official bool | ||||
| } | ||||
		Loading…
	
		Reference in New Issue