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>", Format: "<uuid>",
} }
digestHeader = ParameterDescriptor{
Name: "Docker-Content-Digest",
Description: "Digest of the targeted content for the request.",
Type: "digest",
Format: "<digest>",
}
unauthorizedResponse = ResponseDescriptor{ unauthorizedResponse = ResponseDescriptor{
Description: "The client does not have access to the repository.", Description: "The client does not have access to the repository.",
StatusCode: http.StatusUnauthorized, StatusCode: http.StatusUnauthorized,
@ -454,13 +461,13 @@ var routeDescriptors = []RouteDescriptor{
}, },
{ {
Name: RouteNameManifest, 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", Entity: "Manifest",
Description: "Create, update and retrieve manifests.", Description: "Create, update and retrieve manifests.",
Methods: []MethodDescriptor{ Methods: []MethodDescriptor{
{ {
Method: "GET", 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{ Requests: []RequestDescriptor{
{ {
Headers: []ParameterDescriptor{ Headers: []ParameterDescriptor{
@ -473,8 +480,11 @@ var routeDescriptors = []RouteDescriptor{
}, },
Successes: []ResponseDescriptor{ 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, StatusCode: http.StatusOK,
Headers: []ParameterDescriptor{
digestHeader,
},
Body: BodyDescriptor{ Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8", ContentType: "application/json; charset=utf-8",
Format: manifestBody, Format: manifestBody,
@ -483,7 +493,7 @@ var routeDescriptors = []RouteDescriptor{
}, },
Failures: []ResponseDescriptor{ Failures: []ResponseDescriptor{
{ {
Description: "The name or tag was invalid.", Description: "The name or reference was invalid.",
StatusCode: http.StatusBadRequest, StatusCode: http.StatusBadRequest,
ErrorCodes: []ErrorCode{ ErrorCodes: []ErrorCode{
ErrorCodeNameInvalid, ErrorCodeNameInvalid,
@ -523,7 +533,7 @@ var routeDescriptors = []RouteDescriptor{
}, },
{ {
Method: "PUT", 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{ Requests: []RequestDescriptor{
{ {
Headers: []ParameterDescriptor{ Headers: []ParameterDescriptor{
@ -550,6 +560,7 @@ var routeDescriptors = []RouteDescriptor{
Format: "<url>", Format: "<url>",
}, },
contentLengthZeroHeader, contentLengthZeroHeader,
digestHeader,
}, },
}, },
}, },
@ -628,7 +639,7 @@ var routeDescriptors = []RouteDescriptor{
}, },
{ {
Method: "DELETE", 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{ Requests: []RequestDescriptor{
{ {
Headers: []ParameterDescriptor{ Headers: []ParameterDescriptor{
@ -729,6 +740,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The length of the requested blob content.", Description: "The length of the requested blob content.",
Format: "<length>", Format: "<length>",
}, },
digestHeader,
}, },
Body: BodyDescriptor{ Body: BodyDescriptor{
ContentType: "application/octet-stream", ContentType: "application/octet-stream",
@ -745,6 +757,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The location where the layer should be accessible.", Description: "The location where the layer should be accessible.",
Format: "<blob location>", Format: "<blob location>",
}, },
digestHeader,
}, },
}, },
}, },
@ -1193,6 +1206,7 @@ var routeDescriptors = []RouteDescriptor{
Format: "<length of chunk>", Format: "<length of chunk>",
Description: "Length of the chunk being uploaded, corresponding the length of the request body.", 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 Description: `Generic error returned when the error does not have an
API classification.`, 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, Code: ErrorCodeUnauthorized,
Value: "UNAUTHORIZED", Value: "UNAUTHORIZED",

View file

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

View file

@ -40,7 +40,7 @@ func TestRouter(t *testing.T) {
RequestURI: "/v2/foo/manifests/bar", RequestURI: "/v2/foo/manifests/bar",
Vars: map[string]string{ Vars: map[string]string{
"name": "foo", "name": "foo",
"tag": "bar", "reference": "bar",
}, },
}, },
{ {
@ -48,7 +48,15 @@ func TestRouter(t *testing.T) {
RequestURI: "/v2/foo/bar/manifests/tag", RequestURI: "/v2/foo/bar/manifests/tag",
Vars: map[string]string{ Vars: map[string]string{
"name": "foo/bar", "name": "foo/bar",
"tag": "tag", "reference": "tag",
},
},
{
RouteName: RouteNameManifest,
RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890",
Vars: map[string]string{
"name": "foo/bar",
"reference": "sha256:abcdef01234567890",
}, },
}, },
{ {
@ -113,7 +121,7 @@ func TestRouter(t *testing.T) {
RequestURI: "/v2/foo/bar/manifests/manifests/tags", RequestURI: "/v2/foo/bar/manifests/manifests/tags",
Vars: map[string]string{ Vars: map[string]string{
"name": "foo/bar/manifests", "name": "foo/bar/manifests",
"tag": "tags", "reference": "tags",
}, },
}, },
{ {

View file

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