diff --git a/docs/client/repository.go b/docs/client/repository.go index 4a66f70b..1f360ec8 100644 --- a/docs/client/repository.go +++ b/docs/client/repository.go @@ -75,6 +75,7 @@ func (r *repository) Manifests() distribution.ManifestService { name: r.Name(), ub: r.ub, client: r.client, + etags: make(map[string]string), } } @@ -104,6 +105,7 @@ type manifests struct { name string ub *v2.URLBuilder client *http.Client + etags map[string]string } func (ms *manifests) Tags() ([]string, error) { @@ -173,13 +175,40 @@ func (ms *manifests) Get(dgst digest.Digest) (*manifest.SignedManifest, error) { return ms.GetByTag(dgst.String()) } -func (ms *manifests) GetByTag(tag string) (*manifest.SignedManifest, error) { +// AddEtagToTag allows a client to supply an eTag to GetByTag which will +// be used for a conditional HTTP request. If the eTag matches, a nil +// manifest and nil error will be returned. +func AddEtagToTag(tagName, dgst string) distribution.ManifestServiceOption { + return func(ms distribution.ManifestService) error { + if ms, ok := ms.(*manifests); ok { + ms.etags[tagName] = dgst + return nil + } + return fmt.Errorf("etag options is a client-only option") + } +} + +func (ms *manifests) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*manifest.SignedManifest, error) { + for _, option := range options { + err := option(ms) + if err != nil { + return nil, err + } + } + u, err := ms.ub.BuildManifestURL(ms.name, tag) if err != nil { return nil, err } + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } - resp, err := ms.client.Get(u) + if _, ok := ms.etags[tag]; ok { + req.Header.Set("eTag", ms.etags[tag]) + } + resp, err := ms.client.Do(req) if err != nil { return nil, err } @@ -193,8 +222,9 @@ func (ms *manifests) GetByTag(tag string) (*manifest.SignedManifest, error) { if err := decoder.Decode(&sm); err != nil { return nil, err } - return &sm, nil + case http.StatusNotModified: + return nil, nil default: return nil, handleErrorResponse(resp) } diff --git a/docs/client/repository_test.go b/docs/client/repository_test.go index 7dbe97cf..26d92d8e 100644 --- a/docs/client/repository_test.go +++ b/docs/client/repository_test.go @@ -46,6 +46,7 @@ func newRandomBlob(size int) (digest.Digest, []byte) { } func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { + *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", @@ -60,6 +61,7 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R }), }, }) + *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", @@ -398,6 +400,40 @@ func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*manifest.Signe return m, dgst } +func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) { + actualDigest, _ := digest.FromBytes(content) + getReqWithEtag := testutil.Request{ + Method: "GET", + Route: "/v2/" + repo + "/manifests/" + reference, + Headers: http.Header(map[string][]string{ + "Etag": {dgst}, + }), + } + + var getRespWithEtag testutil.Response + if actualDigest.String() == dgst { + getRespWithEtag = testutil.Response{ + StatusCode: http.StatusNotModified, + Body: []byte{}, + Headers: http.Header(map[string][]string{ + "Content-Length": {"0"}, + "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, + }), + } + } else { + getRespWithEtag = testutil.Response{ + StatusCode: http.StatusOK, + Body: content, + Headers: http.Header(map[string][]string{ + "Content-Length": {fmt.Sprint(len(content))}, + "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, + }), + } + + } + *m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag}) +} + func addTestManifest(repo, reference string, content []byte, m *testutil.RequestResponseMap) { *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ @@ -487,11 +523,11 @@ func TestManifestFetch(t *testing.T) { } } -func TestManifestFetchByTag(t *testing.T) { +func TestManifestFetchWithEtag(t *testing.T) { repo := "test.example.com/repo/by/tag" - m1, _ := newRandomSchemaV1Manifest(repo, "latest", 6) + m1, d1 := newRandomSchemaV1Manifest(repo, "latest", 6) var m testutil.RequestResponseMap - addTestManifest(repo, "latest", m1.Raw, &m) + addTestManifestWithEtag(repo, "latest", m1.Raw, &m, d1.String()) e, c := testServer(m) defer c() @@ -502,20 +538,12 @@ func TestManifestFetchByTag(t *testing.T) { } ms := r.Manifests() - ok, err := ms.ExistsByTag("latest") + m2, err := ms.GetByTag("latest", AddEtagToTag("latest", d1.String())) if err != nil { t.Fatal(err) } - if !ok { - t.Fatal("Manifest does not exist") - } - - manifest, err := ms.GetByTag("latest") - if err != nil { - t.Fatal(err) - } - if err := checkEqualManifest(manifest, m1); err != nil { - t.Fatal(err) + if m2 != nil { + t.Fatal("Expected empty manifest for matching etag") } } diff --git a/docs/storage/manifeststore.go b/docs/storage/manifeststore.go index 07f8de3c..8f6c3562 100644 --- a/docs/storage/manifeststore.go +++ b/docs/storage/manifeststore.go @@ -73,7 +73,14 @@ func (ms *manifestStore) ExistsByTag(tag string) (bool, error) { return ms.tagStore.exists(tag) } -func (ms *manifestStore) GetByTag(tag string) (*manifest.SignedManifest, error) { +func (ms *manifestStore) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*manifest.SignedManifest, error) { + for _, option := range options { + err := option(ms) + if err != nil { + return nil, err + } + } + context.GetLogger(ms.ctx).Debug("(*manifestStore).GetByTag") dgst, err := ms.tagStore.resolve(tag) if err != nil {