package dockerregistry import ( "crypto/tls" "encoding/json" "fmt" "io/ioutil" "net" "net/http" "net/http/cookiejar" "net/url" "path" "strings" "time" "github.com/fsouza/go-dockerclient" "github.com/golang/glog" "k8s.io/kubernetes/pkg/client/transport" knet "k8s.io/kubernetes/pkg/util/net" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" imageapi "github.com/openshift/origin/pkg/image/api" ) type Image struct { docker.Image // Does this registry support pull by ID PullByID bool } // Client includes methods for accessing a Docker registry by name. type Client interface { // Connect to a Docker registry by name. Pass "" for the Docker Hub Connect(registry string, allowInsecure bool) (Connection, error) } // Connection allows you to retrieve data from a Docker V1/V2 registry. type Connection interface { // ImageTags will return a map of the tags for the image by namespace and name. // If namespace is not specified, will default to "library" for Docker hub. ImageTags(namespace, name string) (map[string]string, error) // ImageByID will return the requested image by namespace, name, and ID. // If namespace is not specified, will default to "library" for Docker hub. ImageByID(namespace, name, id string) (*Image, error) // ImageByTag will return the requested image by namespace, name, and tag // (if not specified, "latest"). // If namespace is not specified, will default to "library" for Docker hub. ImageByTag(namespace, name, tag string) (*Image, error) } // client implements the Client interface type client struct { dialTimeout time.Duration connections map[string]*connection allowV2 bool } // NewClient returns a client object which allows public access to // a Docker registry. enableV2 allows a client to prefer V1 registry // API connections. // TODO: accept a docker auth config func NewClient(dialTimeout time.Duration, allowV2 bool) Client { return &client{ dialTimeout: dialTimeout, connections: make(map[string]*connection), allowV2: allowV2, } } // Connect accepts the name of a registry in the common form Docker provides and will // create a connection to the registry. Callers may provide a host, a host:port, or // a fully qualified URL. When not providing a URL, the default scheme will be "https" func (c *client) Connect(name string, allowInsecure bool) (Connection, error) { target, err := normalizeRegistryName(name) if err != nil { return nil, err } prefix := target.String() if conn, ok := c.connections[prefix]; ok && conn.allowInsecure == allowInsecure { return conn, nil } conn := newConnection(*target, c.dialTimeout, allowInsecure, c.allowV2) c.connections[prefix] = conn return conn, nil } // normalizeDockerHubHost returns the canonical DockerHub registry URL for a given host // segment and docker API version. func normalizeDockerHubHost(host string, v2 bool) string { switch host { case imageapi.DockerDefaultRegistry, "www." + imageapi.DockerDefaultRegistry, imageapi.DockerDefaultV1Registry, imageapi.DockerDefaultV2Registry: if v2 { return imageapi.DockerDefaultV2Registry } return imageapi.DockerDefaultV1Registry } return host } // normalizeRegistryName standardizes the registry URL so that it is consistent // across different versions of the same name (for reuse of auth). func normalizeRegistryName(name string) (*url.URL, error) { prefix := name if len(prefix) == 0 { prefix = imageapi.DockerDefaultV1Registry } hadPrefix := false switch { case strings.HasPrefix(prefix, "http://"), strings.HasPrefix(prefix, "https://"): hadPrefix = true default: prefix = "https://" + prefix } target, err := url.Parse(prefix) if err != nil { return nil, fmt.Errorf("the registry name cannot be made into a valid url: %v", err) } if host, port, err := net.SplitHostPort(target.Host); err == nil { host = normalizeDockerHubHost(host, false) if hadPrefix { switch { case port == "443" && target.Scheme == "https": target.Host = host case port == "80" && target.Scheme == "http": target.Host = host } } } else { target.Host = normalizeDockerHubHost(target.Host, false) } return target, nil } // convertConnectionError turns a registry error into a typed error if appropriate. func convertConnectionError(registry string, err error) error { switch { case strings.Contains(err.Error(), "connection refused"): return errRegistryNotFound{registry} default: return err } } // connection represents a connection to a particular DockerHub registry, reusing // tokens and other settings. connections are not thread safe. type connection struct { client *http.Client url url.URL cached map[string]repository isV2 *bool token string allowInsecure bool } // newConnection creates a new connection func newConnection(url url.URL, dialTimeout time.Duration, allowInsecure, enableV2 bool) *connection { var isV2 *bool if !enableV2 { v2 := false isV2 = &v2 } var rt http.RoundTripper if allowInsecure { rt = knet.SetTransportDefaults(&http.Transport{ Dial: (&net.Dialer{ Timeout: dialTimeout, KeepAlive: 30 * time.Second, }).Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }) } else { rt = knet.SetTransportDefaults(&http.Transport{ Dial: (&net.Dialer{ Timeout: dialTimeout, KeepAlive: 30 * time.Second, }).Dial, }) } rt = transport.DebugWrappers(rt) jar, _ := cookiejar.New(nil) client := &http.Client{Jar: jar, Transport: rt} return &connection{ url: url, client: client, cached: make(map[string]repository), isV2: isV2, allowInsecure: allowInsecure, } } // ImageTags returns the tags for the named Docker image repository. func (c *connection) ImageTags(namespace, name string) (map[string]string, error) { if len(namespace) == 0 && imageapi.IsRegistryDockerHub(c.url.Host) { namespace = imageapi.DockerDefaultNamespace } if len(name) == 0 { return nil, fmt.Errorf("image name must be specified") } repo, err := c.getCachedRepository(fmt.Sprintf("%s/%s", namespace, name)) if err != nil { return nil, err } return repo.getTags(c) } // ImageByID returns the specified image within the named Docker image repository func (c *connection) ImageByID(namespace, name, imageID string) (*Image, error) { if len(namespace) == 0 && imageapi.IsRegistryDockerHub(c.url.Host) { namespace = imageapi.DockerDefaultNamespace } if len(name) == 0 { return nil, fmt.Errorf("image name must be specified") } repo, err := c.getCachedRepository(fmt.Sprintf("%s/%s", namespace, name)) if err != nil { return nil, err } return repo.getImage(c, imageID, "") } // ImageByTag returns the specified image within the named Docker image repository func (c *connection) ImageByTag(namespace, name, tag string) (*Image, error) { if len(namespace) == 0 && imageapi.IsRegistryDockerHub(c.url.Host) { namespace = imageapi.DockerDefaultNamespace } if len(name) == 0 { return nil, fmt.Errorf("image name must be specified") } searchTag := tag if len(searchTag) == 0 { searchTag = imageapi.DefaultImageTag } repo, err := c.getCachedRepository(fmt.Sprintf("%s/%s", namespace, name)) if err != nil { return nil, err } return repo.getTaggedImage(c, searchTag, tag) } // getCachedRepository returns a repository interface matching the provided name and // may cache information about the server on the connection object. func (c *connection) getCachedRepository(name string) (repository, error) { if cached, ok := c.cached[name]; ok { return cached, nil } if c.isV2 == nil { v2, err := c.checkV2() if err != nil { return nil, err } c.isV2 = &v2 } if *c.isV2 { base := c.url base.Host = normalizeDockerHubHost(base.Host, true) repo := &v2repository{ name: name, endpoint: base, token: c.token, } c.cached[name] = repo return repo, nil } repo, err := c.getRepositoryV1(name) if err != nil { return nil, err } c.cached[name] = repo return repo, nil } // checkV2 performs the registry version checking steps as described by // https://docs.docker.com/registry/spec/api/ func (c *connection) checkV2() (bool, error) { base := c.url base.Host = normalizeDockerHubHost(base.Host, true) base.Path = path.Join(base.Path, "v2") + "/" req, err := http.NewRequest("GET", base.String(), nil) if err != nil { return false, fmt.Errorf("error creating request: %v", err) } resp, err := c.client.Do(req) if err != nil { // if we tried https and were rejected, try http if c.url.Scheme == "https" && c.allowInsecure { glog.V(4).Infof("Failed to get https, trying http: %v", err) c.url.Scheme = "http" return c.checkV2() } return false, convertConnectionError(c.url.String(), fmt.Errorf("error checking for V2 registry at %s: %v", base.String(), err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusUnauthorized: // handle auth challenges on individual repositories case code >= 300 || resp.StatusCode < 200: return false, nil } if len(resp.Header.Get("Docker-Distribution-API-Version")) == 0 { glog.V(5).Infof("Registry v2 API at %s did not have a Docker-Distribution-API-Version header", base.String()) return false, nil } glog.V(5).Infof("Found registry v2 API at %s", base.String()) return true, nil } // parseAuthChallenge splits a header of the form 'type[ <key>="<value>"[,...]]' returned // by the docker registry func parseAuthChallenge(header string) (string, map[string]string) { sections := strings.SplitN(header, " ", 2) if len(sections) == 1 { sections = append(sections, "") } challenge := sections[1] keys := make(map[string]string) for _, s := range strings.Split(challenge, ",") { pair := strings.SplitN(strings.TrimSpace(s), "=", 2) if len(pair) == 1 { keys[pair[0]] = "" continue } keys[pair[0]] = strings.Trim(pair[1], "\"") } return sections[0], keys } // authenticateV2 attempts to respond to a given WWW-Authenticate challenge header // by asking for a token from the realm. Currently only supports "Bearer" challenges // with no credentials provided. // TODO: support credentials or replace with the Docker distribution v2 registry client func (c *connection) authenticateV2(header string) (string, error) { mode, keys := parseAuthChallenge(header) if strings.ToLower(mode) != "bearer" { return "", fmt.Errorf("unsupported authentication challenge from registry: %s", header) } realm, ok := keys["realm"] if !ok { return "", fmt.Errorf("no realm specified by the server, cannot authenticate: %s", header) } delete(keys, "realm") realmURL, err := url.Parse(realm) if err != nil { return "", fmt.Errorf("realm %q was not a valid url: %v", realm, err) } query := realmURL.Query() for k, v := range keys { query.Set(k, v) } realmURL.RawQuery = query.Encode() req, err := http.NewRequest("GET", realmURL.String(), nil) if err != nil { return "", fmt.Errorf("error creating v2 auth request: %v", err) } resp, err := c.client.Do(req) if err != nil { return "", convertConnectionError(realmURL.String(), fmt.Errorf("error authorizing to the registry: %v", err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusUnauthorized: return "", fmt.Errorf("permission denied to access realm %q", realmURL.String()) case code == http.StatusNotFound: return "", fmt.Errorf("defined realm %q cannot be found", realm) case code >= 300 || resp.StatusCode < 200: return "", fmt.Errorf("error authenticating to the realm %q; server returned %d", realmURL.String(), resp.StatusCode) } token := struct { Token string `json:"token"` }{} body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("can't read authorization body from %s: %v", realmURL.String(), err) } if err := json.Unmarshal(body, &token); err != nil { return "", fmt.Errorf("can't decode the server authorization from %s: %v", realmURL.String(), err) } return token.Token, nil } // getRepositoryV1 returns a repository implementation for a v1 registry by asking for // the appropriate endpoint token. It will try HTTP if HTTPS fails and insecure connections // are allowed. func (c *connection) getRepositoryV1(name string) (repository, error) { glog.V(4).Infof("Getting repository %s from %s", name, c.url) base := c.url base.Path = path.Join(base.Path, fmt.Sprintf("/v1/repositories/%s/images", name)) req, err := http.NewRequest("GET", base.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } req.Header.Add("X-Docker-Token", "true") resp, err := c.client.Do(req) if err != nil { // if we tried https and were rejected, try http if c.url.Scheme == "https" && c.allowInsecure { glog.V(4).Infof("Failed to get https, trying http: %v", err) c.url.Scheme = "http" return c.getRepositoryV1(name) } return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting X-Docker-Token from %s: %v", name, err)) } defer resp.Body.Close() // if we were redirected, update the base urls c.url.Scheme = resp.Request.URL.Scheme c.url.Host = resp.Request.URL.Host switch code := resp.StatusCode; { case code == http.StatusNotFound: return nil, errRepositoryNotFound{name} case code >= 300 || resp.StatusCode < 200: return nil, fmt.Errorf("error retrieving repository: server returned %d", resp.StatusCode) } // TODO: select a random endpoint return &v1repository{ name: name, endpoint: url.URL{Scheme: c.url.Scheme, Host: resp.Header.Get("X-Docker-Endpoints")}, token: resp.Header.Get("X-Docker-Token"), }, nil } // repository is an interface for retrieving image info from a Docker V1 or V2 repository. type repository interface { getTags(c *connection) (map[string]string, error) getTaggedImage(c *connection, tag, userTag string) (*Image, error) getImage(c *connection, image, userTag string) (*Image, error) } // v2repository exposes methods for accessing a named Docker V2 repository on a server. type v2repository struct { name string endpoint url.URL token string retries int } // v2tags describes the tags/list returned by the Docker V2 registry. type v2tags struct { Name string `json:"name"` Tags []string `json:"tags"` } func (repo *v2repository) getTags(c *connection) (map[string]string, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v2/%s/tags/list", repo.name)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } addAcceptHeader(req) if len(repo.token) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", repo.token)) } resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting image tags for %s: %v", repo.name, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusUnauthorized: if len(repo.token) != 0 { // The DockerHub returns JWT tokens that take effect at "now" at second resolution, which means clients can // be rejected when requests are made near the time boundary. if repo.retries > 0 { repo.retries-- time.Sleep(time.Second / 2) return repo.getTags(c) } delete(c.cached, repo.name) // docker will not return a NotFound on any repository URL - for backwards compatibility, return NotFound on the // repo return nil, errRepositoryNotFound{repo.name} } token, err := c.authenticateV2(resp.Header.Get("WWW-Authenticate")) if err != nil { return nil, fmt.Errorf("error getting image tags for %s: %v", repo.name, err) } repo.retries = 2 repo.token = token return repo.getTags(c) case code == http.StatusNotFound: return nil, errRepositoryNotFound{repo.name} case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) return nil, fmt.Errorf("error retrieving tags: server returned %d", resp.StatusCode) } tags := &v2tags{} if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { return nil, fmt.Errorf("error decoding image %s tags: %v", repo.name, err) } legacyTags := make(map[string]string) for _, tag := range tags.Tags { legacyTags[tag] = tag } return legacyTags, nil } func (repo *v2repository) getTaggedImage(c *connection, tag, userTag string) (*Image, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v2/%s/manifests/%s", repo.name, tag)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } addAcceptHeader(req) if len(repo.token) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", repo.token)) } resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting image for %s:%s: %v", repo.name, tag, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusUnauthorized: if len(repo.token) != 0 { // The DockerHub returns JWT tokens that take effect at "now" at second resolution, which means clients can // be rejected when requests are made near the time boundary. if repo.retries > 0 { repo.retries-- time.Sleep(time.Second / 2) return repo.getTaggedImage(c, tag, userTag) } delete(c.cached, repo.name) // docker will not return a NotFound on any repository URL - for backwards compatibility, return NotFound on the // repo body, _ := ioutil.ReadAll(resp.Body) glog.V(4).Infof("passed valid auth token, but unable to find tagged image at %q, %d %v: %s", req.URL.String(), resp.StatusCode, resp.Header, body) return nil, errTagNotFound{len(userTag) == 0, tag, repo.name} } token, err := c.authenticateV2(resp.Header.Get("WWW-Authenticate")) if err != nil { return nil, fmt.Errorf("error getting image for %s:%s: %v", repo.name, tag, err) } repo.retries = 2 repo.token = token return repo.getTaggedImage(c, tag, userTag) case code == http.StatusNotFound: body, _ := ioutil.ReadAll(resp.Body) glog.V(4).Infof("unable to find tagged image at %q, %d %v: %s", req.URL.String(), resp.StatusCode, resp.Header, body) return nil, errTagNotFound{len(userTag) == 0, tag, repo.name} case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) return nil, fmt.Errorf("error retrieving tagged image: server returned %d", resp.StatusCode) } digest := resp.Header.Get("Docker-Content-Digest") body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("can't read image body from %s: %v", req.URL, err) } dockerImage, err := repo.unmarshalImageManifest(c, body) if err != nil { return nil, err } image := &Image{ Image: *dockerImage, } if len(digest) > 0 { image.Image.ID = digest image.PullByID = true } return image, nil } func (repo *v2repository) getImage(c *connection, image, userTag string) (*Image, error) { return repo.getTaggedImage(c, image, userTag) } func (repo *v2repository) getImageConfig(c *connection, dgst string) ([]byte, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v2/%s/blobs/%s", repo.name, dgst)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } if len(repo.token) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", repo.token)) } resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting image config for %s: %v", repo.name, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusUnauthorized: if len(repo.token) != 0 { // The DockerHub returns JWT tokens that take effect at "now" at second resolution, which means clients can // be rejected when requests are made near the time boundary. if repo.retries > 0 { repo.retries-- time.Sleep(time.Second / 2) return repo.getImageConfig(c, dgst) } delete(c.cached, repo.name) // docker will not return a NotFound on any repository URL - for backwards compatibility, return NotFound on the // repo body, _ := ioutil.ReadAll(resp.Body) glog.V(4).Infof("passed valid auth token, but unable to find image config at %q, %d %v: %s", req.URL.String(), resp.StatusCode, resp.Header, body) return nil, errBlobNotFound{dgst, repo.name} } token, err := c.authenticateV2(resp.Header.Get("WWW-Authenticate")) if err != nil { return nil, fmt.Errorf("error getting image config for %s:%s: %v", repo.name, dgst, err) } repo.retries = 2 repo.token = token return repo.getImageConfig(c, dgst) case code == http.StatusNotFound: body, _ := ioutil.ReadAll(resp.Body) glog.V(4).Infof("unable to find image config at %q, %d %v: %s", req.URL.String(), resp.StatusCode, resp.Header, body) return nil, errBlobNotFound{dgst, repo.name} case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) return nil, fmt.Errorf("error retrieving image config: server returned %d", resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("can't read image body from %s: %v", req.URL, err) } return body, nil } func (repo *v2repository) unmarshalImageManifest(c *connection, body []byte) (*docker.Image, error) { manifest := imageapi.DockerImageManifest{} if err := json.Unmarshal(body, &manifest); err != nil { return nil, err } switch manifest.SchemaVersion { case 1: if len(manifest.History) == 0 { return nil, fmt.Errorf("image has no v1Compatibility history and cannot be used") } return unmarshalDockerImage([]byte(manifest.History[0].DockerV1Compatibility)) case 2: config, err := repo.getImageConfig(c, manifest.Config.Digest) if err != nil { return nil, err } return unmarshalDockerImage(config) } return nil, fmt.Errorf("unrecognized Docker image manifest schema %d", manifest.SchemaVersion) } // v1repository exposes methods for accessing a named Docker V1 repository on a server. type v1repository struct { name string endpoint url.URL token string } func (repo *v1repository) getTags(c *connection) (map[string]string, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v1/repositories/%s/tags", repo.name)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } req.Header.Add("Authorization", "Token "+repo.token) resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting image tags for %s: %v", repo.name, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusNotFound: return nil, errRepositoryNotFound{repo.name} case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) return nil, fmt.Errorf("error retrieving tags: server returned %d", resp.StatusCode) } tags := make(map[string]string) if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil { return nil, fmt.Errorf("error decoding image %s tags: %v", repo.name, err) } return tags, nil } func (repo *v1repository) getTaggedImage(c *connection, tag, userTag string) (*Image, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v1/repositories/%s/tags/%s", repo.name, tag)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } req.Header.Add("Authorization", "Token "+repo.token) resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting image id for %s:%s: %v", repo.name, tag, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusNotFound: // Attempt to lookup tag in tags map, supporting registries that don't allow retrieval // of tags to ids (Pulp/Crane) allTags, err := repo.getTags(c) if err != nil { return nil, err } if image, ok := allTags[tag]; ok { return repo.getImage(c, image, "") } body, _ := ioutil.ReadAll(resp.Body) glog.V(4).Infof("unable to find v1 tagged image at %q, %d %v: %s", req.URL.String(), resp.StatusCode, resp.Header, body) return nil, errTagNotFound{len(userTag) == 0, tag, repo.name} case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) return nil, fmt.Errorf("error retrieving tag: server returned %d", resp.StatusCode) } var imageID string if err := json.NewDecoder(resp.Body).Decode(&imageID); err != nil { return nil, fmt.Errorf("error decoding image id: %v", err) } return repo.getImage(c, imageID, "") } func (repo *v1repository) getImage(c *connection, image, userTag string) (*Image, error) { endpoint := repo.endpoint endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("/v1/images/%s/json", image)) req, err := http.NewRequest("GET", endpoint.String(), nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } if len(repo.token) > 0 { req.Header.Add("Authorization", "Token "+repo.token) } resp, err := c.client.Do(req) if err != nil { return nil, convertConnectionError(c.url.String(), fmt.Errorf("error getting json for image %q: %v", image, err)) } defer resp.Body.Close() switch code := resp.StatusCode; { case code == http.StatusNotFound: return nil, NewImageNotFoundError(repo.name, image, userTag) case code >= 300 || resp.StatusCode < 200: // token might have expired - evict repo from cache so we can get a new one on retry delete(c.cached, repo.name) if body, err := ioutil.ReadAll(resp.Body); err == nil { glog.V(6).Infof("unable to fetch image %s: %#v\n%s", req.URL, resp, string(body)) } return nil, fmt.Errorf("error retrieving image %s: server returned %d", req.URL, resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("can't read image body from %s: %v", req.URL, err) } dockerImage, err := unmarshalDockerImage(body) if err != nil { return nil, err } return &Image{Image: *dockerImage}, nil } // errBlobNotFound is an error indicating the requested blob does not exist in the repository. type errBlobNotFound struct { digest string repository string } func (e errBlobNotFound) Error() string { return fmt.Sprintf("blob %s was not found in repository %q", e.digest, e.repository) } // errTagNotFound is an error indicating the requested tag does not exist on the server. May be returned on // a v2 repository when the repository does not exist (because the v2 registry returns 401 on any repository // you do not have permission to see, or does not exist) type errTagNotFound struct { wasDefault bool tag string repository string } func (e errTagNotFound) Error() string { if e.wasDefault { return fmt.Sprintf("the default tag %q has not been set on repository %q", e.tag, e.repository) } return fmt.Sprintf("tag %q has not been set on repository %q", e.tag, e.repository) } // errRepositoryNotFound indicates the repository is not found - but is only guaranteed to be returned // for v1 docker registries. type errRepositoryNotFound struct { repository string } func (e errRepositoryNotFound) Error() string { return fmt.Sprintf("the repository %q was not found", e.repository) } type errImageNotFound struct { tag string image string repository string } func NewImageNotFoundError(repository, image, tag string) error { return errImageNotFound{tag, image, repository} } func (e errImageNotFound) Error() string { if len(e.tag) == 0 { return fmt.Sprintf("the image %q in repository %q was not found and may have been deleted", e.image, e.repository) } return fmt.Sprintf("the image %q in repository %q with tag %q was not found and may have been deleted", e.image, e.repository, e.tag) } type errRegistryNotFound struct { registry string } func (e errRegistryNotFound) Error() string { return fmt.Sprintf("the registry %q could not be reached", e.registry) } func IsRegistryNotFound(err error) bool { _, ok := err.(errRegistryNotFound) return ok } func IsRepositoryNotFound(err error) bool { _, ok := err.(errRepositoryNotFound) return ok } func IsImageNotFound(err error) bool { _, ok := err.(errImageNotFound) return ok } func IsTagNotFound(err error) bool { _, ok := err.(errTagNotFound) return ok } func IsBlobNotFound(err error) bool { _, ok := err.(errBlobNotFound) return ok } func IsNotFound(err error) bool { return IsRegistryNotFound(err) || IsRepositoryNotFound(err) || IsImageNotFound(err) || IsTagNotFound(err) || IsBlobNotFound(err) } func unmarshalDockerImage(body []byte) (*docker.Image, error) { var imagePre012 docker.ImagePre012 if err := json.Unmarshal(body, &imagePre012); err != nil { return nil, err } return &docker.Image{ ID: imagePre012.ID, Parent: imagePre012.Parent, Comment: imagePre012.Comment, Created: imagePre012.Created, Container: imagePre012.Container, ContainerConfig: imagePre012.ContainerConfig, DockerVersion: imagePre012.DockerVersion, Author: imagePre012.Author, Config: imagePre012.Config, Architecture: imagePre012.Architecture, Size: imagePre012.Size, }, nil } func addAcceptHeader(r *http.Request) { r.Header.Add("Accept", schema1.MediaTypeManifest) r.Header.Add("Accept", schema2.MediaTypeManifest) }