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 1bc740b0d5 for server implementation.
Signed-off-by: Richard Scothern <richard.scothern@gmail.com>
			
			
				master
			
			
		
							parent
							
								
									79a4ca2abe
								
							
						
					
					
						commit
						48a2010ac3
					
				|  | @ -93,8 +93,8 @@ func (msl *manifestServiceListener) Put(sm *manifest.SignedManifest) error { | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (msl *manifestServiceListener) GetByTag(tag string) (*manifest.SignedManifest, error) { | func (msl *manifestServiceListener) GetByTag(tag string, options ...distribution.ManifestServiceOption) (*manifest.SignedManifest, error) { | ||||||
| 	sm, err := msl.ManifestService.GetByTag(tag) | 	sm, err := msl.ManifestService.GetByTag(tag, options...) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Name(), sm); err != nil { | 		if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Name(), sm); err != nil { | ||||||
| 			logrus.Errorf("error dispatching manifest pull to listener: %v", err) | 			logrus.Errorf("error dispatching manifest pull to listener: %v", err) | ||||||
|  |  | ||||||
|  | @ -37,6 +37,9 @@ type Namespace interface { | ||||||
| 	Repository(ctx context.Context, name string) (Repository, error) | 	Repository(ctx context.Context, name string) (Repository, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ManifestServiceOption is a function argument for Manifest Service methods
 | ||||||
|  | type ManifestServiceOption func(ManifestService) error | ||||||
|  | 
 | ||||||
| // Repository is a named collection of manifests and layers.
 | // Repository is a named collection of manifests and layers.
 | ||||||
| type Repository interface { | type Repository interface { | ||||||
| 	// Name returns the name of the repository.
 | 	// Name returns the name of the repository.
 | ||||||
|  | @ -84,7 +87,7 @@ type ManifestService interface { | ||||||
| 	ExistsByTag(tag string) (bool, error) | 	ExistsByTag(tag string) (bool, error) | ||||||
| 
 | 
 | ||||||
| 	// GetByTag retrieves the named manifest, if it exists.
 | 	// GetByTag retrieves the named manifest, if it exists.
 | ||||||
| 	GetByTag(tag string) (*manifest.SignedManifest, error) | 	GetByTag(tag string, options ...ManifestServiceOption) (*manifest.SignedManifest, error) | ||||||
| 
 | 
 | ||||||
| 	// TODO(stevvooe): There are several changes that need to be done to this
 | 	// TODO(stevvooe): There are several changes that need to be done to this
 | ||||||
| 	// interface:
 | 	// interface:
 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -21,8 +21,6 @@ type RequestResponseMapping struct { | ||||||
| 	Response Response | 	Response Response | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO(bbland): add support for request headers
 |  | ||||||
| 
 |  | ||||||
| // Request is a simplified http.Request object
 | // Request is a simplified http.Request object
 | ||||||
| type Request struct { | type Request struct { | ||||||
| 	// Method is the http method of the request, for example GET
 | 	// Method is the http method of the request, for example GET
 | ||||||
|  | @ -36,6 +34,9 @@ type Request struct { | ||||||
| 
 | 
 | ||||||
| 	// Body is the byte contents of the http request
 | 	// Body is the byte contents of the http request
 | ||||||
| 	Body []byte | 	Body []byte | ||||||
|  | 
 | ||||||
|  | 	// Headers are the header for this request
 | ||||||
|  | 	Headers http.Header | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r Request) String() string { | func (r Request) String() string { | ||||||
|  | @ -54,7 +55,22 @@ func (r Request) String() string { | ||||||
| 		} | 		} | ||||||
| 		queryString = "?" + strings.Join(queryParts, "&") | 		queryString = "?" + strings.Join(queryParts, "&") | ||||||
| 	} | 	} | ||||||
| 	return fmt.Sprintf("%s %s%s\n%s", r.Method, r.Route, queryString, r.Body) | 	var headers []string | ||||||
|  | 	if len(r.Headers) > 0 { | ||||||
|  | 		var headerKeys []string | ||||||
|  | 		for k := range r.Headers { | ||||||
|  | 			headerKeys = append(headerKeys, k) | ||||||
|  | 		} | ||||||
|  | 		sort.Strings(headerKeys) | ||||||
|  | 
 | ||||||
|  | 		for _, k := range headerKeys { | ||||||
|  | 			for _, val := range r.Headers[k] { | ||||||
|  | 				headers = append(headers, fmt.Sprintf("%s:%s", k, val)) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 	return fmt.Sprintf("%s %s%s\n%s\n%s", r.Method, r.Route, queryString, headers, r.Body) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Response is a simplified http.Response object
 | // Response is a simplified http.Response object
 | ||||||
|  | @ -101,6 +117,14 @@ func (app *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||
| 		Route:       r.URL.Path, | 		Route:       r.URL.Path, | ||||||
| 		QueryParams: r.URL.Query(), | 		QueryParams: r.URL.Query(), | ||||||
| 		Body:        requestBody, | 		Body:        requestBody, | ||||||
|  | 		Headers:     make(map[string][]string), | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Add headers of interest here
 | ||||||
|  | 	for k, v := range r.Header { | ||||||
|  | 		if k == "Etag" { | ||||||
|  | 			request.Headers[k] = v | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	responses, ok := app.responseMap[request.String()] | 	responses, ok := app.responseMap[request.String()] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue