diff --git a/registry/auth.go b/registry/auth.go index bae093ec97..0b2f66cec7 100644 --- a/registry/auth.go +++ b/registry/auth.go @@ -1,7 +1,6 @@ package registry // import "github.com/docker/docker/registry" import ( - "io/ioutil" "net/http" "net/url" "strings" @@ -12,7 +11,6 @@ import ( "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/errdefs" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -22,51 +20,6 @@ const ( 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 := apiEndpoint.ToV1Endpoint(userAgent, nil) - serverAddress := registryEndpoint.String() - - logrus.Debugf("attempting v1 login to registry endpoint %s", serverAddress) - - if serverAddress == "" { - return "", "", errdefs.System(errors.New("server Error: Server Address not set")) - } - - req, err := http.NewRequest(http.MethodGet, 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 "", "", errdefs.System(err) - } - - switch resp.StatusCode { - case http.StatusOK: - return "Login Succeeded", "", nil - case http.StatusUnauthorized: - return "", "", errdefs.Unauthorized(errors.New("Wrong login/password, please try again")) - case http.StatusForbidden: - // *TODO: Use registry configuration to determine what this says, if anything? - return "", "", errdefs.Forbidden(errors.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)) - case http.StatusInternalServerError: - logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body) - return "", "", errdefs.System(errors.New("Internal Server Error")) - } - return "", "", errdefs.System(errors.Errorf("Login: %s (Code: %d; Headers: %s)", body, - resp.StatusCode, resp.Header)) -} - type loginCredentialStore struct { authConfig *types.AuthConfig } diff --git a/registry/auth_test.go b/registry/auth_test.go index f8f3e1997b..0a9cdd4bd3 100644 --- a/registry/auth_test.go +++ b/registry/auth_test.go @@ -20,20 +20,6 @@ func buildAuthConfigs() map[string]types.AuthConfig { 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] diff --git a/registry/config.go b/registry/config.go index 6bb9258c9b..54b83fa40a 100644 --- a/registry/config.go +++ b/registry/config.go @@ -26,7 +26,7 @@ type serviceConfig struct { registrytypes.ServiceConfig } -var ( +const ( // DefaultNamespace is the default namespace DefaultNamespace = "docker.io" // DefaultRegistryVersionHeader is the name of the default HTTP header @@ -39,29 +39,26 @@ var ( IndexServer = "https://" + IndexHostname + "/v1/" // IndexName is the name of the index IndexName = "docker.io" +) +var ( // 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{}) -) + validHostPortRegex = regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) -var ( - validHostPortRegex = regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) + // for mocking in unit tests + lookupIP = net.LookupIP ) -// for mocking in unit tests -var lookupIP = net.LookupIP - // newServiceConfig returns a new instance of ServiceConfig func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { config := &serviceConfig{ diff --git a/registry/errors.go b/registry/errors.go index 5bab02e5e2..4906303efc 100644 --- a/registry/errors.go +++ b/registry/errors.go @@ -7,14 +7,6 @@ import ( "github.com/docker/docker/errdefs" ) -type notFoundError string - -func (e notFoundError) Error() string { - return string(e) -} - -func (notFoundError) NotFound() {} - func translateV2AuthError(err error) error { switch e := err.(type) { case *url.Error: diff --git a/registry/registry_test.go b/registry/registry_test.go index d0659c17fc..417c9574bc 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -1,10 +1,8 @@ package registry // import "github.com/docker/docker/registry" import ( - "fmt" "net/http" "net/http/httputil" - "net/url" "os" "strings" "testing" @@ -17,15 +15,6 @@ import ( "gotest.tools/v3/skip" ) -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) @@ -50,7 +39,7 @@ func spawnTestRegistrySession(t *testing.T) *Session { // 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 + r.client.Transport.(*authTransport).token = []string{"fake-token"} return r } @@ -148,153 +137,6 @@ func TestEndpoint(t *testing.T) { } } -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.ParseNormalizedNamed(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.ParseNormalizedNamed("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.ParseNormalizedNamed(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.ParseNormalizedNamed("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.ParseNormalizedNamed(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 @@ -701,50 +543,6 @@ func TestMirrorEndpointLookup(t *testing.T) { } } -func TestPushRegistryTag(t *testing.T) { - r := spawnTestRegistrySession(t) - repoRef, err := reference.ParseNormalizedNamed(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.ParseNormalizedNamed(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) diff --git a/registry/service.go b/registry/service.go index 08f5c7a4e1..3b08e39da2 100644 --- a/registry/service.go +++ b/registry/service.go @@ -108,36 +108,35 @@ func (s *DefaultService) LoadInsecureRegistries(registries []string) error { // 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 "", "", errdefs.InvalidParameter(errors.Errorf("unable to parse server address: %v", err)) + var registryHostName = IndexHostname + + if authConfig.ServerAddress != "" { + serverAddress := authConfig.ServerAddress + if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { + serverAddress = "https://" + serverAddress + } + u, err := url.Parse(serverAddress) + if err != nil { + return "", "", errdefs.InvalidParameter(errors.Errorf("unable to parse server address: %v", err)) + } + registryHostName = u.Host } - endpoints, err := s.LookupPushEndpoints(u.Host) + // Lookup endpoints for authentication using "LookupPushEndpoints", which + // excludes mirrors to prevent sending credentials of the upstream registry + // to a mirror. + endpoints, err := s.LookupPushEndpoints(registryHostName) if err != nil { return "", "", errdefs.InvalidParameter(err) } for _, endpoint := range endpoints { - login := loginV2 - if endpoint.Version == APIVersion1 { - login = loginV1 - } - - status, token, err = login(authConfig, endpoint, userAgent) + status, token, err = loginV2(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) + logrus.WithError(fErr.err).Infof("Error logging in to endpoint, trying next endpoint") continue } @@ -150,18 +149,13 @@ func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, // 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] + // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), + // use the default Docker Hub registry (docker.io) + return IndexName, reposName } - return indexName, remoteName + return nameParts[0], nameParts[1] } // Search queries the public registry for images matching the specified @@ -184,7 +178,7 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut } // *TODO: Search multiple indexes. - endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers)) + endpoint, err := NewV1Endpoint(index, userAgent, headers) if err != nil { return nil, err } @@ -228,13 +222,8 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut 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) + // If pull "library/foo", it's stored locally under "foo" + remoteName = strings.TrimPrefix(remoteName, "library/") } return r.SearchRepositories(remoteName, limit) } @@ -259,6 +248,7 @@ type APIEndpoint struct { } // ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint +// Deprecated: this function is deprecated and will be removed in a future update func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) *V1Endpoint { return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders) } @@ -280,24 +270,22 @@ func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, er 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. +// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. +// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP. func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() - return s.lookupEndpoints(hostname) + return s.lookupV2Endpoints(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. +// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. +// It gives preference to HTTPS over plain HTTP. Mirrors are not included. func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) { s.mu.Lock() defer s.mu.Unlock() - allEndpoints, err := s.lookupEndpoints(hostname) + allEndpoints, err := s.lookupV2Endpoints(hostname) if err == nil { for _, endpoint := range allEndpoints { if !endpoint.Mirror { @@ -307,7 +295,3 @@ func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEn } return endpoints, err } - -func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) { - return s.lookupV2Endpoints(hostname) -} diff --git a/registry/service_v2.go b/registry/service_v2.go index 1a4c9e3105..3e3a5b41ff 100644 --- a/registry/service_v2.go +++ b/registry/service_v2.go @@ -10,7 +10,6 @@ import ( func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) { tlsConfig := tlsconfig.ServerDefault() if hostname == DefaultNamespace || hostname == IndexHostname { - // v2 mirrors for _, mirror := range s.config.Mirrors { if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { mirror = "https://" + mirror @@ -24,15 +23,13 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp return nil, err } endpoints = append(endpoints, APIEndpoint{ - URL: mirrorURL, - // guess mirrors are v2 + URL: mirrorURL, Version: APIVersion2, Mirror: true, TrimHostname: true, TLSConfig: mirrorTLSConfig, }) } - // v2 registry endpoints = append(endpoints, APIEndpoint{ URL: DefaultV2Registry, Version: APIVersion2, diff --git a/registry/session.go b/registry/session.go index 14825c5f9a..d34dc1e58a 100644 --- a/registry/session.go +++ b/registry/session.go @@ -1,43 +1,26 @@ package registry // import "github.com/docker/docker/registry" import ( - "bytes" - "crypto/sha256" - // 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" "sync" - "github.com/docker/distribution/reference" - "github.com/docker/distribution/registry/api/errcode" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/tarsum" - "github.com/docker/docker/registry/resumable" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -var ( - // ErrRepoNotFound is returned if the repository didn't exist on the - // remote side - ErrRepoNotFound notFoundError = "Repository not found" -) - // A Session is used to communicate with a V1 registry type Session struct { indexEndpoint *V1Endpoint @@ -214,527 +197,6 @@ func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1E 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 != http.StatusOK { - if res.StatusCode == http.StatusUnauthorized { - return nil, errcode.ErrorCodeUnauthorized.WithArgs() - } - return nil, newJSONError(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 != http.StatusOK { - return newJSONError(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 != http.StatusOK { - return nil, -1, newJSONError(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(http.MethodGet, imageURL, nil) - if err != nil { - return nil, fmt.Errorf("Error while getting from the server: %v", err) - } - - 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 != http.StatusOK { - 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 resumable.NewRequestReaderWithInitialResponse(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 := reference.Path(repositoryRef) - - 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 != http.StatusOK { - 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 := reference.Path(repositoryRef) - - 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 != http.StatusOK { - 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(), reference.Path(name)) - - logrus.Debugf("[registry] Calling GET %s", repositoryTarget) - - req, err := http.NewRequest(http.MethodGet, 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 == http.StatusUnauthorized { - 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, newJSONError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res) - } else if res.StatusCode != http.StatusOK { - errBody, err := ioutil.ReadAll(res.Body) - if err != nil { - logrus.Debugf("Error reading response body: %s", err) - } - return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, reference.Path(name), 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(http.MethodPut, 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 != http.StatusOK { - 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(http.MethodPut, 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 == http.StatusUnauthorized && strings.HasPrefix(registry, "http://") { - return newJSONError("HTTP code 401, Docker will not send auth headers over HTTP.", res) - } - if res.StatusCode != http.StatusOK { - errBody, err := ioutil.ReadAll(res.Body) - if err != nil { - return newJSONError(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 newJSONError(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(http.MethodPut, 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 != http.StatusOK { - errBody, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", "", newJSONError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res) - } - return "", "", newJSONError(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 '/ -func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registry string) error { - // "jsonify" the string - revision = "\"" + revision + "\"" - path := fmt.Sprintf("repositories/%s/tags/%s", reference.Path(remote), tag) - - req, err := http.NewRequest(http.MethodPut, 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 != http.StatusOK && res.StatusCode != http.StatusCreated { - return newJSONError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, reference.Path(remote)), 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(), reference.Path(remote), 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 == http.StatusUnauthorized { - return nil, errcode.ErrorCodeUnauthorized.WithArgs() - } - - var tokens, endpoints []string - if !validate { - if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated { - errBody, err := ioutil.ReadAll(res.Body) - if err != nil { - logrus.Debugf("Error reading response body: %s", err) - } - return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, reference.Path(remote), 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 != http.StatusNoContent { - errBody, err := ioutil.ReadAll(res.Body) - if err != nil { - logrus.Debugf("Error reading response body: %s", err) - } - return nil, newJSONError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, reference.Path(remote), 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(http.MethodPut, 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 { @@ -755,28 +217,11 @@ func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.Sea } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, newJSONError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res) + return nil, &jsonmessage.JSONError{ + Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode), + Code: res.StatusCode, + } } result := new(registrytypes.SearchResults) return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results") } - -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() -} - -func newJSONError(msg string, res *http.Response) error { - return &jsonmessage.JSONError{ - Message: msg, - Code: res.StatusCode, - } -}