Skip to content

Commit

Permalink
Merge pull request distribution#211 from stevvooe/immutable-manifest-…
Browse files Browse the repository at this point in the history
…references

doc/spec, registry: immutable manifest reference support
  • Loading branch information
stevvooe committed Mar 6, 2015
2 parents 0ecb468 + 008236c commit 0233da8
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 114 deletions.
33 changes: 27 additions & 6 deletions docs/api/v2/descriptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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{
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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{
Expand All @@ -550,6 +560,7 @@ var routeDescriptors = []RouteDescriptor{
Format: "<url>",
},
contentLengthZeroHeader,
digestHeader,
},
},
},
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -729,6 +740,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The length of the requested blob content.",
Format: "<length>",
},
digestHeader,
},
Body: BodyDescriptor{
ContentType: "application/octet-stream",
Expand All @@ -745,6 +757,7 @@ var routeDescriptors = []RouteDescriptor{
Description: "The location where the layer should be accessible.",
Format: "<blob location>",
},
digestHeader,
},
},
},
Expand Down Expand Up @@ -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,
},
},
},
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions docs/api/v2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 14 additions & 6 deletions docs/api/v2/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
{
Expand Down Expand Up @@ -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",
},
},
{
Expand Down
7 changes: 4 additions & 3 deletions docs/api/v2/urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
68 changes: 64 additions & 4 deletions docs/handlers/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ func TestLayerAPI(t *testing.T) {

checkResponse(t, "checking head on existing layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)},
"Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{layerDigest.String()},
})

// ----------------
Expand All @@ -230,7 +231,8 @@ func TestLayerAPI(t *testing.T) {

checkResponse(t, "fetching layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)},
"Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{layerDigest.String()},
})

// Verify the body
Expand Down Expand Up @@ -286,6 +288,9 @@ func TestManifestAPI(t *testing.T) {
// --------------------------------
// Attempt to push unsigned manifest with missing layers
unsignedManifest := &manifest.Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 1,
},
Name: imageName,
Tag: tag,
FSLayers: []manifest.FSLayer{
Expand Down Expand Up @@ -343,16 +348,43 @@ func TestManifestAPI(t *testing.T) {
t.Fatalf("unexpected error signing manifest: %v", err)
}

payload, err := signedManifest.Payload()
checkErr(t, err, "getting manifest payload")

dgst, err := digest.FromBytes(payload)
checkErr(t, err, "digesting manifest")

manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url")

resp = putManifest(t, "putting signed manifest", manifestURL, signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusAccepted)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
"Docker-Content-Digest": []string{dgst.String()},
})

// --------------------
// Push by digest -- should get same result
resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusAccepted)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
"Docker-Content-Digest": []string{dgst.String()},
})

// ------------------
// Fetch by tag name
resp, err = http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error fetching manifest: %v", err)
}
defer resp.Body.Close()

checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
})

var fetchedManifest manifest.SignedManifest
dec := json.NewDecoder(resp.Body)
Expand All @@ -364,6 +396,27 @@ func TestManifestAPI(t *testing.T) {
t.Fatalf("manifests do not match")
}

// ---------------
// Fetch by digest
resp, err = http.Get(manifestDigestURL)
checkErr(t, err, "fetching manifest by digest")
defer resp.Body.Close()

checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
})

var fetchedManifestByDigest manifest.SignedManifest
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifestByDigest); err != nil {
t.Fatalf("error decoding fetched manifest: %v", err)
}

if !bytes.Equal(fetchedManifestByDigest.Raw, signedManifest.Raw) {
t.Fatalf("manifests do not match")
}

// Ensure that the tag is listed.
resp, err = http.Get(tagsURL)
if err != nil {
Expand Down Expand Up @@ -534,8 +587,9 @@ func pushLayer(t *testing.T, ub *v2.URLBuilder, name string, dgst digest.Digest,
}

checkHeaders(t, resp, http.Header{
"Location": []string{expectedLayerURL},
"Content-Length": []string{"0"},
"Location": []string{expectedLayerURL},
"Content-Length": []string{"0"},
"Docker-Content-Digest": []string{dgst.String()},
})

return resp.Header.Get("Location")
Expand Down Expand Up @@ -634,3 +688,9 @@ func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) {
}
}
}

func checkErr(t *testing.T, err error, msg string) {
if err != nil {
t.Fatalf("unexpected error %s: %v", msg, err)
}
}
3 changes: 1 addition & 2 deletions docs/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,8 @@ func (app *App) context(w http.ResponseWriter, r *http.Request) *Context {
ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx))
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx,
"vars.name",
"vars.tag",
"vars.reference",
"vars.digest",
"vars.tag",
"vars.uuid"))

context := &Context{
Expand Down
2 changes: 1 addition & 1 deletion docs/handlers/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestAppDispatcher(t *testing.T) {
endpoint: v2.RouteNameManifest,
vars: []string{
"name", "foo/bar",
"tag", "sometag",
"reference", "sometag",
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions docs/handlers/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ func getName(ctx context.Context) (name string) {
return ctxu.GetStringValue(ctx, "vars.name")
}

func getTag(ctx context.Context) (tag string) {
return ctxu.GetStringValue(ctx, "vars.tag")
func getReference(ctx context.Context) (reference string) {
return ctxu.GetStringValue(ctx, "vars.reference")
}

var errDigestNotAvailable = fmt.Errorf("digest not available in context")
Expand Down
Loading

0 comments on commit 0233da8

Please sign in to comment.