forked from TrueCloudLab/distribution
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:
parent
0ecb468a33
commit
f46a1b73e8
4 changed files with 48 additions and 15 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue