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>",
|
||||
}
|
||||
|
||||
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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue