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>
			
			
				master
			
			
		
							parent
							
								
									af3f674323
								
							
						
					
					
						commit
						caf989a572
					
				| 
						 | 
					@ -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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue