diff --git a/docs/handlers/api_test.go b/docs/handlers/api_test.go index 9952d68ef..8d6319417 100644 --- a/docs/handlers/api_test.go +++ b/docs/handlers/api_test.go @@ -449,6 +449,7 @@ func TestManifestAPI(t *testing.T) { checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, + "ETag": []string{dgst.String()}, }) var fetchedManifest manifest.SignedManifest @@ -470,6 +471,7 @@ func TestManifestAPI(t *testing.T) { checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, + "ETag": []string{dgst.String()}, }) var fetchedManifestByDigest manifest.SignedManifest @@ -482,6 +484,33 @@ func TestManifestAPI(t *testing.T) { t.Fatalf("manifests do not match") } + // Get by name with etag, gives 304 + etag := resp.Header.Get("Etag") + req, err := http.NewRequest("GET", manifestURL, nil) + if err != nil { + t.Fatalf("Error constructing request: %s", err) + } + req.Header.Set("If-None-Match", etag) + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("Error constructing request: %s", err) + } + + checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) + + // Get by digest with etag, gives 304 + req, err = http.NewRequest("GET", manifestDigestURL, nil) + if err != nil { + t.Fatalf("Error constructing request: %s", err) + } + req.Header.Set("If-None-Match", etag) + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("Error constructing request: %s", err) + } + + checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) + // Ensure that the tag is listed. resp, err = http.Get(tagsURL) if err != nil { diff --git a/docs/handlers/images.go b/docs/handlers/images.go index 41fbabc43..747b2780e 100644 --- a/docs/handlers/images.go +++ b/docs/handlers/images.go @@ -60,6 +60,10 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http if imh.Tag != "" { sm, err = manifests.GetByTag(imh.Tag) } else { + if etagMatch(r, imh.Digest.String()) { + w.WriteHeader(http.StatusNotModified) + return + } sm, err = manifests.Get(imh.Digest) } @@ -75,6 +79,10 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) return } + if etagMatch(r, dgst.String()) { + w.WriteHeader(http.StatusNotModified) + return + } imh.Digest = dgst } @@ -82,9 +90,19 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Length", fmt.Sprint(len(sm.Raw))) w.Header().Set("Docker-Content-Digest", imh.Digest.String()) + w.Header().Set("Etag", imh.Digest.String()) w.Write(sm.Raw) } +func etagMatch(r *http.Request, etag string) bool { + for _, headerVal := range r.Header["If-None-Match"] { + if headerVal == etag { + return true + } + } + return false +} + // PutImageManifest validates and stores and image in the registry. func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(imh).Debug("PutImageManifest")