Add API unit testing for schema2 manifest

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2015-12-16 14:30:49 -08:00
parent 3f746a8207
commit 9284810356
2 changed files with 383 additions and 44 deletions

View file

@ -18,11 +18,13 @@ import (
"strings"
"testing"
"github.com/docker/distribution"
"github.com/docker/distribution/configuration"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/distribution/registry/api/v2"
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
@ -690,48 +692,40 @@ func httpDelete(url string) (*http.Response, error) {
}
type manifestArgs struct {
imageName string
signedManifest *schema1.SignedManifest
dgst digest.Digest
}
func makeManifestArgs(t *testing.T) manifestArgs {
args := manifestArgs{
imageName: "foo/bar",
}
return args
imageName string
mediaType string
manifest distribution.Manifest
dgst digest.Digest
}
func TestManifestAPI(t *testing.T) {
deleteEnabled := false
env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t)
testManifestAPI(t, env, args)
testManifestAPISchema1(t, env, "foo/schema1")
testManifestAPISchema2(t, env, "foo/schema2")
deleteEnabled = true
env = newTestEnv(t, deleteEnabled)
args = makeManifestArgs(t)
testManifestAPI(t, env, args)
testManifestAPISchema1(t, env, "foo/schema1")
testManifestAPISchema2(t, env, "foo/schema2")
}
func TestManifestDelete(t *testing.T) {
deleteEnabled := true
env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t)
env, args = testManifestAPI(t, env, args)
testManifestDelete(t, env, args)
schema1Args := testManifestAPISchema1(t, env, "foo/schema1")
testManifestDelete(t, env, schema1Args)
schema2Args := testManifestAPISchema2(t, env, "foo/schema2")
testManifestDelete(t, env, schema2Args)
}
func TestManifestDeleteDisabled(t *testing.T) {
deleteEnabled := false
env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t)
testManifestDeleteDisabled(t, env, args)
testManifestDeleteDisabled(t, env, "foo/schema1")
}
func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) *testEnv {
imageName := args.imageName
func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName string) {
manifestURL, err := env.builder.BuildManifestURL(imageName, digest.DigestSha256EmptyTar)
if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err)
@ -744,12 +738,11 @@ func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) *
defer resp.Body.Close()
checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed)
return nil
}
func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, manifestArgs) {
imageName := args.imageName
func testManifestAPISchema1(t *testing.T, env *testEnv, imageName string) manifestArgs {
tag := "thetag"
args := manifestArgs{imageName: imageName}
manifestURL, err := env.builder.BuildManifestURL(imageName, tag)
if err != nil {
@ -808,10 +801,10 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
},
}
resp = putManifest(t, "putting unsigned manifest", manifestURL, unsignedManifest)
resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest)
defer resp.Body.Close()
checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest)
_, p, counts := checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestInvalid)
_, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid)
expectedCounts := map[errcode.ErrorCode]int{
v2.ErrorCodeManifestInvalid: 1,
@ -827,7 +820,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
t.Fatalf("error signing manifest: %v", err)
}
resp = putManifest(t, "putting signed manifest with errors", manifestURL, sm)
resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm)
defer resp.Body.Close()
checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest)
_, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp,
@ -872,13 +865,13 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
}
dgst := digest.FromBytes(signedManifest.Canonical)
args.signedManifest = signedManifest
args.manifest = signedManifest
args.dgst = dgst
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url")
resp = putManifest(t, "putting signed manifest no error", manifestURL, signedManifest)
resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest)
checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
@ -887,7 +880,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
// --------------------
// Push by digest -- should get same result
resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest)
resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
@ -958,7 +951,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
}
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, sm2)
resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "", sm2)
checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
resp, err = http.Get(manifestDigestURL)
@ -1020,8 +1013,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
}
defer resp.Body.Close()
// Check that we get an unknown repository error when asking for tags
checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK)
checkResponse(t, "getting tags", resp, http.StatusOK)
dec = json.NewDecoder(resp.Body)
var tagsResponse tagsAPIResponse
@ -1052,16 +1044,359 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
t.Fatalf("error signing manifest")
}
resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, invalidSigned)
resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned)
checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest)
return env, args
return args
}
func testManifestAPISchema2(t *testing.T, env *testEnv, imageName string) manifestArgs {
tag := "schema2tag"
args := manifestArgs{
imageName: imageName,
mediaType: schema2.MediaTypeManifest,
}
manifestURL, err := env.builder.BuildManifestURL(imageName, tag)
if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err)
}
// -----------------------------
// Attempt to fetch the manifest
resp, err := http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error getting manifest: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound)
checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown)
tagsURL, err := env.builder.BuildTagsURL(imageName)
if err != nil {
t.Fatalf("unexpected error building tags url: %v", err)
}
resp, err = http.Get(tagsURL)
if err != nil {
t.Fatalf("unexpected error getting unknown tags: %v", err)
}
defer resp.Body.Close()
// Check that we get an unknown repository error when asking for tags
checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound)
checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown)
// --------------------------------
// Attempt to push manifest with missing config and missing layers
manifest := &schema2.Manifest{
Versioned: manifest.Versioned{
SchemaVersion: 2,
},
MediaType: schema2.MediaTypeManifest,
Config: distribution.Descriptor{
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 3253,
MediaType: schema2.MediaTypeConfig,
},
Layers: []distribution.Descriptor{
{
Digest: "sha256:463434349086340864309863409683460843608348608934092322395278926a",
Size: 6323,
MediaType: schema2.MediaTypeLayer,
},
{
Digest: "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa",
Size: 6863,
MediaType: schema2.MediaTypeLayer,
},
},
}
resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest)
defer resp.Body.Close()
checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest)
_, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown)
expectedCounts := map[errcode.ErrorCode]int{
v2.ErrorCodeManifestBlobUnknown: 3,
}
if !reflect.DeepEqual(counts, expectedCounts) {
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
}
// Push a config, and reference it in the manifest
sampleConfig := []byte(`{
"architecture": "amd64",
"history": [
{
"created": "2015-10-31T22:22:54.690851953Z",
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
},
{
"created": "2015-10-31T22:22:55.613815829Z",
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]"
}
],
"rootfs": {
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
],
"type": "layers"
}
}`)
sampleConfigDigest := digest.FromBytes(sampleConfig)
uploadURLBase, _ := startPushLayer(t, env.builder, imageName)
pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig))
manifest.Config.Digest = sampleConfigDigest
manifest.Config.Size = int64(len(sampleConfig))
// The manifest should still be invalid, because its layer doesnt exist
resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest)
defer resp.Body.Close()
checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest)
_, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown)
expectedCounts = map[errcode.ErrorCode]int{
v2.ErrorCodeManifestBlobUnknown: 2,
}
if !reflect.DeepEqual(counts, expectedCounts) {
t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p))
}
// Push 2 random layers
expectedLayers := make(map[digest.Digest]io.ReadSeeker)
for i := range manifest.Layers {
rs, dgstStr, err := testutil.CreateRandomTarFile()
if err != nil {
t.Fatalf("error creating random layer %d: %v", i, err)
}
dgst := digest.Digest(dgstStr)
expectedLayers[dgst] = rs
manifest.Layers[i].Digest = dgst
uploadURLBase, _ := startPushLayer(t, env.builder, imageName)
pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs)
}
// -------------------
// Push the manifest with all layers pushed.
deserializedManifest, err := schema2.FromStruct(*manifest)
if err != nil {
t.Fatalf("could not create DeserializedManifest: %v", err)
}
_, canonical, err := deserializedManifest.Payload()
if err != nil {
t.Fatalf("could not get manifest payload: %v", err)
}
dgst := digest.FromBytes(canonical)
args.dgst = dgst
args.manifest = deserializedManifest
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url")
resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest)
checkResponse(t, "putting manifest no error", resp, http.StatusCreated)
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 manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest)
checkResponse(t, "putting manifest by digest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
"Docker-Content-Digest": []string{dgst.String()},
})
// ------------------
// Fetch by tag name
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
req.Header.Set("Accept", schema2.MediaTypeManifest)
resp, err = http.DefaultClient.Do(req)
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()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
var fetchedManifest schema2.DeserializedManifest
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifest); err != nil {
t.Fatalf("error decoding fetched manifest: %v", err)
}
_, fetchedCanonical, err := fetchedManifest.Payload()
if err != nil {
t.Fatalf("error getting manifest payload: %v", err)
}
if !bytes.Equal(fetchedCanonical, canonical) {
t.Fatalf("manifests do not match")
}
// ---------------
// Fetch by digest
req, err = http.NewRequest("GET", manifestDigestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
req.Header.Set("Accept", schema2.MediaTypeManifest)
resp, err = http.DefaultClient.Do(req)
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()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
var fetchedManifestByDigest schema2.DeserializedManifest
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifestByDigest); err != nil {
t.Fatalf("error decoding fetched manifest: %v", err)
}
_, fetchedCanonical, err = fetchedManifest.Payload()
if err != nil {
t.Fatalf("error getting manifest payload: %v", err)
}
if !bytes.Equal(fetchedCanonical, canonical) {
t.Fatalf("manifests do not match")
}
// Get by name with etag, gives 304
etag := resp.Header.Get("Etag")
req, err = http.NewRequest("GET", manifestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
req.Header.Set("If-None-Match", etag)
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified)
// Get by digest with etag, gives 304
req, err = http.NewRequest("GET", manifestDigestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
req.Header.Set("If-None-Match", etag)
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified)
// Ensure that the tag is listed.
resp, err = http.Get(tagsURL)
if err != nil {
t.Fatalf("unexpected error getting unknown tags: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK)
dec = json.NewDecoder(resp.Body)
var tagsResponse tagsAPIResponse
if err := dec.Decode(&tagsResponse); err != nil {
t.Fatalf("unexpected error decoding error response: %v", err)
}
if tagsResponse.Name != imageName {
t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName)
}
if len(tagsResponse.Tags) != 1 {
t.Fatalf("expected some tags in response: %v", tagsResponse.Tags)
}
if tagsResponse.Tags[0] != tag {
t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag)
}
// ------------------
// Fetch as a schema1 manifest
resp, err = http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error fetching manifest as schema1: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
var fetchedSchema1Manifest schema1.SignedManifest
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedSchema1Manifest); err != nil {
t.Fatalf("error decoding fetched schema1 manifest: %v", err)
}
if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 {
t.Fatal("wrong schema version")
}
if fetchedSchema1Manifest.Architecture != "amd64" {
t.Fatal("wrong architecture")
}
if fetchedSchema1Manifest.Name != imageName {
t.Fatal("wrong image name")
}
if fetchedSchema1Manifest.Tag != tag {
t.Fatal("wrong tag")
}
if len(fetchedSchema1Manifest.FSLayers) != 2 {
t.Fatal("wrong number of FSLayers")
}
for i := range manifest.Layers {
if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest {
t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i)
}
}
if len(fetchedSchema1Manifest.History) != 2 {
t.Fatal("wrong number of History entries")
}
// Don't check V1Compatibility fields becuase we're using randomly-generated
// layers.
return args
}
func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
imageName := args.imageName
dgst := args.dgst
signedManifest := args.signedManifest
manifest := args.manifest
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
// ---------------
// Delete by digest
@ -1090,8 +1425,8 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
// --------------------
// Re-upload manifest by digest
resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest)
checkResponse(t, "putting manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL},
"Docker-Content-Digest": []string{dgst.String()},
@ -1183,7 +1518,7 @@ func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *te
}
}
func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response {
var body []byte
if sm, ok := v.(*schema1.SignedManifest); ok {
@ -1205,6 +1540,10 @@ func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
t.Fatalf("error creating request for %s: %v", msg, err)
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("error doing put request while %s: %v", msg, err)
@ -1532,7 +1871,7 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string)
location, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building location URL")
resp := putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest)
resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest)
checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{
"Location": []string{location},
@ -1570,7 +1909,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) {
t.Fatalf("error signing manifest: %v", err)
}
resp := putManifest(t, "putting unsigned manifest", manifestURL, sm)
resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm)
checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
// Manifest Delete

View file

@ -86,7 +86,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
// Only rewrite schema2 manifests when they are being fetched by tag.
// If they are being fetched by digest, we can't return something not
// matching the digest.
if _, isSchema2 := manifest.(*schema2.DeserializedManifest); imh.Tag != "" && isSchema2 {
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); imh.Tag != "" && isSchema2 {
supportsSchema2 := false
if acceptHeaders, ok := r.Header["Accept"]; ok {
for _, mediaType := range acceptHeaders {
@ -101,7 +101,7 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
// Rewrite manifest in schema1 format
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
targetDescriptor := manifest.Target()
targetDescriptor := schema2Manifest.Target()
blobs := imh.Repository.Blobs(imh)
configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
if err != nil {