spec: fetch manifests by tag or digest

Manifests are now fetched by a field called "reference", which may be a tag or
a digest. When using digests to reference a manifest, the data is immutable.
The routes and specification have been updated to allow this.

There are a few caveats to this approach:

1. It may be problematic to rely on data format to differentiate between a tag
   and a digest. Currently, they are disjoint but there may modifications on
   either side that break this guarantee.
2. The caching characteristics of returned content are very different for
   digest versus tag-based references. Digest urls can be cached forever while tag
   urls cannot.

Both of these are minimal caveats that we can live with in the future.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2015-02-25 18:04:28 -08:00
parent 0ecb468a33
commit f46a1b73e8
4 changed files with 48 additions and 15 deletions

View file

@ -79,6 +79,13 @@ var (
Format: "<uuid>",
}
digestHeader = ParameterDescriptor{
Name: "Docker-Content-Digest",
Description: "Digest of the targeted content for the request.",
Type: "digest",
Format: "<digest>",
}
unauthorizedResponse = ResponseDescriptor{
Description: "The client does not have access to the repository.",
StatusCode: http.StatusUnauthorized,
@ -454,13 +461,13 @@ var routeDescriptors = []RouteDescriptor{
},
{
Name: RouteNameManifest,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{tag:" + TagNameRegexp.String() + "}",
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
Entity: "Manifest",
Description: "Create, update and retrieve manifests.",
Methods: []MethodDescriptor{
{
Method: "GET",
Description: "Fetch the manifest identified by `name` and `tag`.",
Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
Requests: []RequestDescriptor{
{
Headers: []ParameterDescriptor{
@ -473,8 +480,11 @@ var routeDescriptors = []RouteDescriptor{
},
Successes: []ResponseDescriptor{
{
Description: "The manifest idenfied by `name` and `tag`. The contents can be used to identify and resolve resources required to run the specified image.",
Description: "The manifest idenfied by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.",
StatusCode: http.StatusOK,
Headers: []ParameterDescriptor{
digestHeader,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: manifestBody,
@ -483,7 +493,7 @@ var routeDescriptors = []RouteDescriptor{
},
Failures: []ResponseDescriptor{
{
Description: "The name or tag was invalid.",
Description: "The name or reference was invalid.",
StatusCode: http.StatusBadRequest,
ErrorCodes: []ErrorCode{
ErrorCodeNameInvalid,
@ -523,7 +533,7 @@ var routeDescriptors = []RouteDescriptor{
},
{
Method: "PUT",
Description: "Put the manifest identified by `name` and `tag`.",
Description: "Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
Requests: []RequestDescriptor{
{
Headers: []ParameterDescriptor{
@ -550,6 +560,7 @@ var routeDescriptors = []RouteDescriptor{
Format: "<url>",
},
contentLengthZeroHeader,
digestHeader,
},
},
},
@ -628,7 +639,7 @@ var routeDescriptors = []RouteDescriptor{
},
{
Method: "DELETE",
Description: "Delete the manifest identified by `name` and `tag`.",
Description: "Delete the manifest identified by `name` and `reference` where `reference` can be a tag or digest.",
Requests: []RequestDescriptor{
{
Headers: []ParameterDescriptor{
@ -729,6 +740,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The length of the requested blob content.",
Format: "<length>",
},
digestHeader,
},
Body: BodyDescriptor{
ContentType: "application/octet-stream",
@ -745,6 +757,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The location where the layer should be accessible.",
Format: "<blob location>",
},
digestHeader,
},
},
},
@ -1193,6 +1206,7 @@ var routeDescriptors = []RouteDescriptor{
Format: "<length of chunk>",
Description: "Length of the chunk being uploaded, corresponding the length of the request body.",
},
digestHeader,
},
},
},
@ -1312,6 +1326,13 @@ var errorDescriptors = []ErrorDescriptor{
Description: `Generic error returned when the error does not have an
API classification.`,
},
{
Code: ErrorCodeUnsupported,
Value: "UNSUPPORTED",
Message: "The operation is unsupported.",
Description: `The operation was unsupported due to a missing
implementation or invalid set of parameters.`,
},
{
Code: ErrorCodeUnauthorized,
Value: "UNAUTHORIZED",

View file

@ -13,6 +13,9 @@ const (
// ErrorCodeUnknown is a catch-all for errors not defined below.
ErrorCodeUnknown ErrorCode = iota
// ErrorCodeUnsupported is returned when an operation is not supported.
ErrorCodeUnsupported
// ErrorCodeUnauthorized is returned if a request is not authorized.
ErrorCodeUnauthorized

View file

@ -39,16 +39,24 @@ func TestRouter(t *testing.T) {
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/manifests/bar",
Vars: map[string]string{
"name": "foo",
"tag": "bar",
"name": "foo",
"reference": "bar",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/tag",
Vars: map[string]string{
"name": "foo/bar",
"tag": "tag",
"name": "foo/bar",
"reference": "tag",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
Vars: map[string]string{
"name": "foo/bar",
"reference": "sha256:abcdef01234567890",
},
},
{
@ -112,8 +120,8 @@ func TestRouter(t *testing.T) {
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/manifests/tags",
Vars: map[string]string{
"name": "foo/bar/manifests",
"tag": "tags",
"name": "foo/bar/manifests",
"reference": "tags",
},
},
{

View file

@ -107,11 +107,12 @@ func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
return tagsURL.String(), nil
}
// BuildManifestURL constructs a url for the manifest identified by name and tag.
func (ub *URLBuilder) BuildManifestURL(name, tag string) (string, error) {
// BuildManifestURL constructs a url for the manifest identified by name and
// reference. The argument reference may be either a tag or digest.
func (ub *URLBuilder) BuildManifestURL(name, reference string) (string, error) {
route := ub.cloneRoute(RouteNameManifest)
manifestURL, err := route.URL("name", name, "tag", tag)
manifestURL, err := route.URL("name", name, "reference", reference)
if err != nil {
return "", err
}