Allow conditional fetching of manifests with the registry client.

Add a functional argument to pass a digest to (ManifestService).GetByTag().
If the digest matches an empty manifest and nil error are returned.

See 6bedf7d1cd for server implementation.

Signed-off-by: Richard Scothern <richard.scothern@gmail.com>
This commit is contained in:
Richard Scothern 2015-07-14 16:25:37 -07:00
parent af3f674323
commit caf989a572
3 changed files with 83 additions and 18 deletions

View file

@ -75,6 +75,7 @@ func (r *repository) Manifests() distribution.ManifestService {
name: r.Name(), name: r.Name(),
ub: r.ub, ub: r.ub,
client: r.client, client: r.client,
etags: make(map[string]string),
} }
} }
@ -104,6 +105,7 @@ type manifests struct {
name string name string
ub *v2.URLBuilder ub *v2.URLBuilder
client *http.Client client *http.Client
etags map[string]string
} }
func (ms *manifests) Tags() ([]string, error) { 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()) 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) u, err := ms.ub.BuildManifestURL(ms.name, tag)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@ -193,8 +222,9 @@ func (ms *manifests) GetByTag(tag string) (*manifest.SignedManifest, error) {
if err := decoder.Decode(&sm); err != nil { if err := decoder.Decode(&sm); err != nil {
return nil, err return nil, err
} }
return &sm, nil return &sm, nil
case http.StatusNotModified:
return nil, nil
default: default:
return nil, handleErrorResponse(resp) return nil, handleErrorResponse(resp)
} }

View file

@ -46,6 +46,7 @@ func newRandomBlob(size int) (digest.Digest, []byte) {
} }
func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
*m = append(*m, testutil.RequestResponseMapping{ *m = append(*m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
Method: "GET", Method: "GET",
@ -60,6 +61,7 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R
}), }),
}, },
}) })
*m = append(*m, testutil.RequestResponseMapping{ *m = append(*m, testutil.RequestResponseMapping{
Request: testutil.Request{ Request: testutil.Request{
Method: "HEAD", Method: "HEAD",
@ -398,6 +400,40 @@ func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*manifest.Signe
return m, dgst 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) { func addTestManifest(repo, reference string, content []byte, m *testutil.RequestResponseMap) {
*m = append(*m, testutil.RequestResponseMapping{ *m = append(*m, testutil.RequestResponseMapping{
Request: testutil.Request{ 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" repo := "test.example.com/repo/by/tag"
m1, _ := newRandomSchemaV1Manifest(repo, "latest", 6) m1, d1 := newRandomSchemaV1Manifest(repo, "latest", 6)
var m testutil.RequestResponseMap var m testutil.RequestResponseMap
addTestManifest(repo, "latest", m1.Raw, &m) addTestManifestWithEtag(repo, "latest", m1.Raw, &m, d1.String())
e, c := testServer(m) e, c := testServer(m)
defer c() defer c()
@ -502,20 +538,12 @@ func TestManifestFetchByTag(t *testing.T) {
} }
ms := r.Manifests() ms := r.Manifests()
ok, err := ms.ExistsByTag("latest") m2, err := ms.GetByTag("latest", AddEtagToTag("latest", d1.String()))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !ok { if m2 != nil {
t.Fatal("Manifest does not exist") t.Fatal("Expected empty manifest for matching etag")
}
manifest, err := ms.GetByTag("latest")
if err != nil {
t.Fatal(err)
}
if err := checkEqualManifest(manifest, m1); err != nil {
t.Fatal(err)
} }
} }

View file

@ -73,7 +73,14 @@ func (ms *manifestStore) ExistsByTag(tag string) (bool, error) {
return ms.tagStore.exists(tag) 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") context.GetLogger(ms.ctx).Debug("(*manifestStore).GetByTag")
dgst, err := ms.tagStore.resolve(tag) dgst, err := ms.tagStore.resolve(tag)
if err != nil { if err != nil {