Merge pull request #1281 from aaronlehmann/new-manifest

Implement schema2 manifest formats
This commit is contained in:
Stephen Day 2016-01-07 17:19:56 -08:00
commit c426367881
8 changed files with 1167 additions and 213 deletions

View file

@ -18,11 +18,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/distribution"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1" "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/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
_ "github.com/docker/distribution/registry/storage/driver/inmemory" _ "github.com/docker/distribution/registry/storage/driver/inmemory"
@ -690,48 +693,42 @@ func httpDelete(url string) (*http.Response, error) {
} }
type manifestArgs struct { type manifestArgs struct {
imageName string imageName string
signedManifest *schema1.SignedManifest mediaType string
dgst digest.Digest manifest distribution.Manifest
} dgst digest.Digest
func makeManifestArgs(t *testing.T) manifestArgs {
args := manifestArgs{
imageName: "foo/bar",
}
return args
} }
func TestManifestAPI(t *testing.T) { func TestManifestAPI(t *testing.T) {
deleteEnabled := false deleteEnabled := false
env := newTestEnv(t, deleteEnabled) env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t) testManifestAPISchema1(t, env, "foo/schema1")
testManifestAPI(t, env, args) schema2Args := testManifestAPISchema2(t, env, "foo/schema2")
testManifestAPIManifestList(t, env, schema2Args)
deleteEnabled = true deleteEnabled = true
env = newTestEnv(t, deleteEnabled) env = newTestEnv(t, deleteEnabled)
args = makeManifestArgs(t) testManifestAPISchema1(t, env, "foo/schema1")
testManifestAPI(t, env, args) schema2Args = testManifestAPISchema2(t, env, "foo/schema2")
testManifestAPIManifestList(t, env, schema2Args)
} }
func TestManifestDelete(t *testing.T) { func TestManifestDelete(t *testing.T) {
deleteEnabled := true deleteEnabled := true
env := newTestEnv(t, deleteEnabled) env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t) schema1Args := testManifestAPISchema1(t, env, "foo/schema1")
env, args = testManifestAPI(t, env, args) testManifestDelete(t, env, schema1Args)
testManifestDelete(t, env, args) schema2Args := testManifestAPISchema2(t, env, "foo/schema2")
testManifestDelete(t, env, schema2Args)
} }
func TestManifestDeleteDisabled(t *testing.T) { func TestManifestDeleteDisabled(t *testing.T) {
deleteEnabled := false deleteEnabled := false
env := newTestEnv(t, deleteEnabled) env := newTestEnv(t, deleteEnabled)
args := makeManifestArgs(t) testManifestDeleteDisabled(t, env, "foo/schema1")
testManifestDeleteDisabled(t, env, args)
} }
func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) *testEnv { func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName string) {
imageName := args.imageName
manifestURL, err := env.builder.BuildManifestURL(imageName, digest.DigestSha256EmptyTar) manifestURL, err := env.builder.BuildManifestURL(imageName, digest.DigestSha256EmptyTar)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err) t.Fatalf("unexpected error getting manifest url: %v", err)
@ -744,12 +741,11 @@ func testManifestDeleteDisabled(t *testing.T, env *testEnv, args manifestArgs) *
defer resp.Body.Close() defer resp.Body.Close()
checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed) checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed)
return nil
} }
func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, manifestArgs) { func testManifestAPISchema1(t *testing.T, env *testEnv, imageName string) manifestArgs {
imageName := args.imageName
tag := "thetag" tag := "thetag"
args := manifestArgs{imageName: imageName}
manifestURL, err := env.builder.BuildManifestURL(imageName, tag) manifestURL, err := env.builder.BuildManifestURL(imageName, tag)
if err != nil { if err != nil {
@ -808,10 +804,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() defer resp.Body.Close()
checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest) 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{ expectedCounts := map[errcode.ErrorCode]int{
v2.ErrorCodeManifestInvalid: 1, v2.ErrorCodeManifestInvalid: 1,
@ -827,7 +823,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
t.Fatalf("error signing manifest: %v", err) 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() defer resp.Body.Close()
checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest) checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest)
_, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp, _, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp,
@ -872,13 +868,13 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
} }
dgst := digest.FromBytes(signedManifest.Canonical) dgst := digest.FromBytes(signedManifest.Canonical)
args.signedManifest = signedManifest args.manifest = signedManifest
args.dgst = dgst args.dgst = dgst
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url") 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) checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL}, "Location": []string{manifestDigestURL},
@ -887,7 +883,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
// -------------------- // --------------------
// Push by digest -- should get same result // 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) checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL}, "Location": []string{manifestDigestURL},
@ -958,7 +954,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) checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated)
resp, err = http.Get(manifestDigestURL) resp, err = http.Get(manifestDigestURL)
@ -1020,8 +1016,7 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
} }
defer resp.Body.Close() defer resp.Body.Close()
// Check that we get an unknown repository error when asking for tags checkResponse(t, "getting tags", resp, http.StatusOK)
checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK)
dec = json.NewDecoder(resp.Body) dec = json.NewDecoder(resp.Body)
var tagsResponse tagsAPIResponse var tagsResponse tagsAPIResponse
@ -1052,16 +1047,581 @@ func testManifestAPI(t *testing.T, env *testEnv, args manifestArgs) (*testEnv, m
t.Fatalf("error signing manifest") 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) 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 testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) {
imageName := args.imageName
tag := "manifestlisttag"
manifestURL, err := env.builder.BuildManifestURL(imageName, tag)
if err != nil {
t.Fatalf("unexpected error getting manifest url: %v", err)
}
// --------------------------------
// Attempt to push manifest list that refers to an unknown manifest
manifestList := &manifestlist.ManifestList{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: manifestlist.MediaTypeManifestList,
},
Manifests: []manifestlist.ManifestDescriptor{
{
Descriptor: distribution.Descriptor{
Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
Size: 3253,
MediaType: schema2.MediaTypeManifest,
},
Platform: manifestlist.PlatformSpec{
Architecture: "amd64",
OS: "linux",
},
},
},
}
resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList)
defer resp.Body.Close()
checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest)
_, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown)
expectedCounts := map[errcode.ErrorCode]int{
v2.ErrorCodeManifestBlobUnknown: 1,
}
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 manifest list that references an actual manifest
manifestList.Manifests[0].Digest = args.dgst
deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests)
if err != nil {
t.Fatalf("could not create DeserializedManifestList: %v", err)
}
_, canonical, err := deserializedManifestList.Payload()
if err != nil {
t.Fatalf("could not get manifest list payload: %v", err)
}
dgst := digest.FromBytes(canonical)
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building manifest url")
resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList)
checkResponse(t, "putting manifest list 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 list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList)
checkResponse(t, "putting manifest list 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", manifestlist.MediaTypeManifestList)
req.Header.Add("Accept", schema1.MediaTypeManifest)
req.Header.Add("Accept", schema2.MediaTypeManifest)
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("unexpected error fetching manifest list: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
var fetchedManifestList manifestlist.DeserializedManifestList
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifestList); err != nil {
t.Fatalf("error decoding fetched manifest list: %v", err)
}
_, fetchedCanonical, err := fetchedManifestList.Payload()
if err != nil {
t.Fatalf("error getting manifest list payload: %v", err)
}
if !bytes.Equal(fetchedCanonical, canonical) {
t.Fatalf("manifest lists 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", manifestlist.MediaTypeManifestList)
resp, err = http.DefaultClient.Do(req)
checkErr(t, err, "fetching manifest list by digest")
defer resp.Body.Close()
checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
var fetchedManifestListByDigest manifestlist.DeserializedManifestList
dec = json.NewDecoder(resp.Body)
if err := dec.Decode(&fetchedManifestListByDigest); err != nil {
t.Fatalf("error decoding fetched manifest: %v", err)
}
_, fetchedCanonical, err = fetchedManifestListByDigest.Payload()
if err != nil {
t.Fatalf("error getting manifest list 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)
// ------------------
// Fetch as a schema1 manifest
resp, err = http.Get(manifestURL)
if err != nil {
t.Fatalf("unexpected error fetching manifest list as schema1: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "fetching uploaded manifest list 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")
}
layers := args.manifest.(*schema2.DeserializedManifest).Layers
for i := range layers {
if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(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.
} }
func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
imageName := args.imageName imageName := args.imageName
dgst := args.dgst dgst := args.dgst
signedManifest := args.signedManifest manifest := args.manifest
manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String()) manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
// --------------- // ---------------
// Delete by digest // Delete by digest
@ -1090,8 +1650,8 @@ func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) {
// -------------------- // --------------------
// Re-upload manifest by digest // Re-upload manifest by digest
resp = putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest) resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest)
checkResponse(t, "putting signed manifest", resp, http.StatusCreated) checkResponse(t, "putting manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Location": []string{manifestDigestURL}, "Location": []string{manifestDigestURL},
"Docker-Content-Digest": []string{dgst.String()}, "Docker-Content-Digest": []string{dgst.String()},
@ -1183,16 +1743,23 @@ 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 var body []byte
if sm, ok := v.(*schema1.SignedManifest); ok { switch m := v.(type) {
_, pl, err := sm.Payload() case *schema1.SignedManifest:
_, pl, err := m.Payload()
if err != nil { if err != nil {
t.Fatalf("error getting payload: %v", err) t.Fatalf("error getting payload: %v", err)
} }
body = pl body = pl
} else { case *manifestlist.DeserializedManifestList:
_, pl, err := m.Payload()
if err != nil {
t.Fatalf("error getting payload: %v", err)
}
body = pl
default:
var err error var err error
body, err = json.MarshalIndent(v, "", " ") body, err = json.MarshalIndent(v, "", " ")
if err != nil { if err != nil {
@ -1205,6 +1772,10 @@ func putManifest(t *testing.T, msg, url string, v interface{}) *http.Response {
t.Fatalf("error creating request for %s: %v", msg, err) t.Fatalf("error creating request for %s: %v", msg, err)
} }
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
t.Fatalf("error doing put request while %s: %v", msg, err) t.Fatalf("error doing put request while %s: %v", msg, err)
@ -1532,7 +2103,7 @@ func createRepository(env *testEnv, t *testing.T, imageName string, tag string)
location, err := env.builder.BuildManifestURL(imageName, dgst.String()) location, err := env.builder.BuildManifestURL(imageName, dgst.String())
checkErr(t, err, "building location URL") 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) checkResponse(t, "putting signed manifest", resp, http.StatusCreated)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Location": []string{location}, "Location": []string{location},
@ -1570,7 +2141,7 @@ func TestRegistryAsCacheMutationAPIs(t *testing.T) {
t.Fatalf("error signing manifest: %v", err) 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) checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode)
// Manifest Delete // Manifest Delete

View file

@ -30,6 +30,7 @@ import (
storagedriver "github.com/docker/distribution/registry/storage/driver" storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/factory" "github.com/docker/distribution/registry/storage/driver/factory"
storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware"
"github.com/docker/libtrust"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -67,10 +68,15 @@ type App struct {
redis *redis.Pool redis *redis.Pool
// true if this registry is configured as a pull through cache // trustKey is a deprecated key used to sign manifests converted to
// schema1 for backward compatibility. It should not be used for any
// other purposes.
trustKey libtrust.PrivateKey
// isCache is true if this registry is configured as a pull through cache
isCache bool isCache bool
// true if the registry is in a read-only maintenance mode // readOnly is true if the registry is in a read-only maintenance mode
readOnly bool readOnly bool
} }
@ -139,6 +145,13 @@ func NewApp(ctx context.Context, configuration *configuration.Configuration) *Ap
app.configureRedis(configuration) app.configureRedis(configuration)
app.configureLogHook(configuration) app.configureLogHook(configuration)
// Generate an ephemeral key to be used for signing converted manifests
// for clients that don't support schema2.
app.trustKey, err = libtrust.GenerateECP256PrivateKey()
if err != nil {
panic(err)
}
if configuration.HTTP.Host != "" { if configuration.HTTP.Host != "" {
u, err := url.Parse(configuration.HTTP.Host) u, err := url.Parse(configuration.HTTP.Host)
if err != nil { if err != nil {

View file

@ -8,11 +8,21 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/manifestlist"
"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/errcode"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
) )
// These constants determine which architecture and OS to choose from a
// manifest list when downconverting it to a schema1 manifest.
const (
defaultArch = "amd64"
defaultOS = "linux"
)
// imageManifestDispatcher takes the request context and builds the // imageManifestDispatcher takes the request context and builds the
// appropriate handler for handling image manifest requests. // appropriate handler for handling image manifest requests.
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
@ -51,8 +61,6 @@ type imageManifestHandler struct {
} }
// GetImageManifest fetches the image manifest from the storage backend, if it exists. // GetImageManifest fetches the image manifest from the storage backend, if it exists.
// todo(richardscothern): this assumes v2 schema 1 manifests for now but in the future
// get the version from the Accept HTTP header
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(imh).Debug("GetImageManifest") ctxu.GetLogger(imh).Debug("GetImageManifest")
manifests, err := imh.Repository.Manifests(imh) manifests, err := imh.Repository.Manifests(imh)
@ -83,6 +91,67 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
return return
} }
supportsSchema2 := false
supportsManifestList := false
if acceptHeaders, ok := r.Header["Accept"]; ok {
for _, mediaType := range acceptHeaders {
if mediaType == schema2.MediaTypeManifest {
supportsSchema2 = true
}
if mediaType == manifestlist.MediaTypeManifestList {
supportsManifestList = true
}
}
}
schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
// 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 imh.Tag != "" && isSchema2 && !supportsSchema2 {
// Rewrite manifest in schema1 format
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
if err != nil {
return
}
} else if imh.Tag != "" && isManifestList && !supportsManifestList {
// Rewrite manifest in schema1 format
ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
// Find the image manifest corresponding to the default
// platform
var manifestDigest digest.Digest
for _, manifestDescriptor := range manifestList.Manifests {
if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
manifestDigest = manifestDescriptor.Digest
break
}
}
if manifestDigest == "" {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
return
}
manifest, err = manifests.Get(imh, manifestDigest)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
return
}
// If necessary, convert the image manifest
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
if err != nil {
return
}
}
}
ct, p, err := manifest.Payload() ct, p, err := manifest.Payload()
if err != nil { if err != nil {
return return
@ -95,6 +164,31 @@ func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http
w.Write(p) w.Write(p)
} }
func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
targetDescriptor := schema2Manifest.Target()
blobs := imh.Repository.Blobs(imh)
configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
return nil, err
}
builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, imh.Repository.Name(), imh.Tag, configJSON)
for _, d := range schema2Manifest.References() {
if err := builder.AppendReference(d); err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
return nil, err
}
}
manifest, err := builder.Build(imh)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
return nil, err
}
return manifest, nil
}
func etagMatch(r *http.Request, etag string) bool { func etagMatch(r *http.Request, etag string) bool {
for _, headerVal := range r.Header["If-None-Match"] { for _, headerVal := range r.Header["If-None-Match"] {
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted

View file

@ -0,0 +1,96 @@
package storage
import (
"fmt"
"encoding/json"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/manifestlist"
)
// manifestListHandler is a ManifestHandler that covers schema2 manifest lists.
type manifestListHandler struct {
repository *repository
blobStore *linkedBlobStore
ctx context.Context
}
var _ ManifestHandler = &manifestListHandler{}
func (ms *manifestListHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Unmarshal")
var m manifestlist.DeserializedManifestList
if err := json.Unmarshal(content, &m); err != nil {
return nil, err
}
return &m, nil
}
func (ms *manifestListHandler) Put(ctx context.Context, manifestList distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put")
m, ok := manifestList.(*manifestlist.DeserializedManifestList)
if !ok {
return "", fmt.Errorf("wrong type put to manifestListHandler: %T", manifestList)
}
if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil {
return "", err
}
mt, payload, err := m.Payload()
if err != nil {
return "", err
}
revision, err := ms.blobStore.Put(ctx, mt, payload)
if err != nil {
context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
return "", err
}
// Link the revision into the repository.
if err := ms.blobStore.linkBlob(ctx, revision); err != nil {
return "", err
}
return revision.Digest, nil
}
// verifyManifest ensures that the manifest content is valid from the
// perspective of the registry. As a policy, the registry only tries to
// store valid content, leaving trust policies of that content up to
// consumers.
func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst manifestlist.DeserializedManifestList, skipDependencyVerification bool) error {
var errs distribution.ErrManifestVerification
if !skipDependencyVerification {
// This manifest service is different from the blob service
// returned by Blob. It uses a linked blob store to ensure that
// only manifests are accessible.
manifestService, err := ms.repository.Manifests(ctx)
if err != nil {
return err
}
for _, manifestDescriptor := range mnfst.References() {
exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest)
if err != nil && err != distribution.ErrBlobUnknown {
errs = append(errs, err)
}
if err != nil || !exists {
// On error here, we always append unknown blob errors.
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: manifestDescriptor.Digest})
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}

View file

@ -1,24 +1,53 @@
package storage package storage
import ( import (
"encoding/json"
"fmt" "fmt"
"encoding/json"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference" "github.com/docker/distribution/manifest/schema2"
"github.com/docker/libtrust"
) )
// manifestStore is a storage driver based store for storing schema1 manifests. // A ManifestHandler gets and puts manifests of a particular type.
type ManifestHandler interface {
// Unmarshal unmarshals the manifest from a byte slice.
Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error)
// Put creates or updates the given manifest returning the manifest digest.
Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error)
}
// SkipLayerVerification allows a manifest to be Put before its
// layers are on the filesystem
func SkipLayerVerification() distribution.ManifestServiceOption {
return skipLayerOption{}
}
type skipLayerOption struct{}
func (o skipLayerOption) Apply(m distribution.ManifestService) error {
if ms, ok := m.(*manifestStore); ok {
ms.skipDependencyVerification = true
return nil
}
return fmt.Errorf("skip layer verification only valid for manifestStore")
}
type manifestStore struct { type manifestStore struct {
repository *repository repository *repository
blobStore *linkedBlobStore blobStore *linkedBlobStore
ctx context.Context ctx context.Context
signatures *signatureStore
skipDependencyVerification bool skipDependencyVerification bool
schema1Handler ManifestHandler
schema2Handler ManifestHandler
manifestListHandler ManifestHandler
} }
var _ distribution.ManifestService = &manifestStore{} var _ distribution.ManifestService = &manifestStore{}
@ -40,18 +69,6 @@ func (ms *manifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool,
func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
context.GetLogger(ms.ctx).Debug("(*manifestStore).Get") context.GetLogger(ms.ctx).Debug("(*manifestStore).Get")
// Ensure that this revision is available in this repository.
_, err := ms.blobStore.Stat(ctx, dgst)
if err != nil {
if err == distribution.ErrBlobUnknown {
return nil, distribution.ErrManifestUnknownRevision{
Name: ms.repository.Name(),
Revision: dgst,
}
}
return nil, err
}
// TODO(stevvooe): Need to check descriptor from above to ensure that the // TODO(stevvooe): Need to check descriptor from above to ensure that the
// mediatype is as we expect for the manifest store. // mediatype is as we expect for the manifest store.
@ -68,84 +85,42 @@ func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ..
return nil, err return nil, err
} }
// Fetch the signatures for the manifest var versioned manifest.Versioned
signatures, err := ms.signatures.Get(dgst) if err = json.Unmarshal(content, &versioned); err != nil {
if err != nil {
return nil, err return nil, err
} }
jsig, err := libtrust.NewJSONSignature(content, signatures...) switch versioned.SchemaVersion {
if err != nil { case 1:
return nil, err return ms.schema1Handler.Unmarshal(ctx, dgst, content)
case 2:
// This can be an image manifest or a manifest list
switch versioned.MediaType {
case schema2.MediaTypeManifest:
return ms.schema2Handler.Unmarshal(ctx, dgst, content)
case manifestlist.MediaTypeManifestList:
return ms.manifestListHandler.Unmarshal(ctx, dgst, content)
default:
return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)}
}
} }
// Extract the pretty JWS return nil, fmt.Errorf("unrecognized manifest schema version %d", versioned.SchemaVersion)
raw, err := jsig.PrettySignature("signatures")
if err != nil {
return nil, err
}
var sm schema1.SignedManifest
if err := json.Unmarshal(raw, &sm); err != nil {
return nil, err
}
return &sm, nil
}
// SkipLayerVerification allows a manifest to be Put before its
// layers are on the filesystem
func SkipLayerVerification() distribution.ManifestServiceOption {
return skipLayerOption{}
}
type skipLayerOption struct{}
func (o skipLayerOption) Apply(m distribution.ManifestService) error {
if ms, ok := m.(*manifestStore); ok {
ms.skipDependencyVerification = true
return nil
}
return fmt.Errorf("skip layer verification only valid for manifestStore")
} }
func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
context.GetLogger(ms.ctx).Debug("(*manifestStore).Put") context.GetLogger(ms.ctx).Debug("(*manifestStore).Put")
sm, ok := manifest.(*schema1.SignedManifest) switch manifest.(type) {
if !ok { case *schema1.SignedManifest:
return "", fmt.Errorf("non-v1 manifest put to signed manifestStore: %T", manifest) return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification)
case *schema2.DeserializedManifest:
return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification)
case *manifestlist.DeserializedManifestList:
return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification)
} }
if err := ms.verifyManifest(ms.ctx, *sm); err != nil { return "", fmt.Errorf("unrecognized manifest type %T", manifest)
return "", err
}
mt := schema1.MediaTypeManifest
payload := sm.Canonical
revision, err := ms.blobStore.Put(ctx, mt, payload)
if err != nil {
context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
return "", err
}
// Link the revision into the repository.
if err := ms.blobStore.linkBlob(ctx, revision); err != nil {
return "", err
}
// Grab each json signature and store them.
signatures, err := sm.Signatures()
if err != nil {
return "", err
}
if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
return "", err
}
return revision.Digest, nil
} }
// Delete removes the revision of the specified manfiest. // Delete removes the revision of the specified manfiest.
@ -157,64 +132,3 @@ func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error {
func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { func (ms *manifestStore) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
return 0, distribution.ErrUnsupported return 0, distribution.ErrUnsupported
} }
// verifyManifest ensures that the manifest content is valid from the
// perspective of the registry. It ensures that the signature is valid for the
// enclosed payload. As a policy, the registry only tries to store valid
// content, leaving trust policies of that content up to consumems.
func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest) error {
var errs distribution.ErrManifestVerification
if len(mnfst.Name) > reference.NameTotalLengthMax {
errs = append(errs,
distribution.ErrManifestNameInvalid{
Name: mnfst.Name,
Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax),
})
}
if !reference.NameRegexp.MatchString(mnfst.Name) {
errs = append(errs,
distribution.ErrManifestNameInvalid{
Name: mnfst.Name,
Reason: fmt.Errorf("invalid manifest name format"),
})
}
if len(mnfst.History) != len(mnfst.FSLayers) {
errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d",
len(mnfst.History), len(mnfst.FSLayers)))
}
if _, err := schema1.Verify(&mnfst); err != nil {
switch err {
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
errs = append(errs, distribution.ErrManifestUnverified{})
default:
if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust
errs = append(errs, distribution.ErrManifestUnverified{})
} else {
errs = append(errs, err)
}
}
}
if !ms.skipDependencyVerification {
for _, fsLayer := range mnfst.References() {
_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest)
if err != nil {
if err != distribution.ErrBlobUnknown {
errs = append(errs, err)
}
// On error here, we always append unknown blob erroms.
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}

View file

@ -165,28 +165,45 @@ func (repo *repository) Manifests(ctx context.Context, options ...distribution.M
blobLinkPath, blobLinkPath,
} }
blobStore := &linkedBlobStore{
ctx: ctx,
blobStore: repo.blobStore,
repository: repo,
deleteEnabled: repo.registry.deleteEnabled,
blobAccessController: &linkedBlobStatter{
blobStore: repo.blobStore,
repository: repo,
linkPathFns: manifestLinkPathFns,
},
// TODO(stevvooe): linkPath limits this blob store to only
// manifests. This instance cannot be used for blob checks.
linkPathFns: manifestLinkPathFns,
}
ms := &manifestStore{ ms := &manifestStore{
ctx: ctx, ctx: ctx,
repository: repo, repository: repo,
blobStore: &linkedBlobStore{ blobStore: blobStore,
ctx: ctx, schema1Handler: &signedManifestHandler{
blobStore: repo.blobStore,
repository: repo,
deleteEnabled: repo.registry.deleteEnabled,
blobAccessController: &linkedBlobStatter{
blobStore: repo.blobStore,
repository: repo,
linkPathFns: manifestLinkPathFns,
},
// TODO(stevvooe): linkPath limits this blob store to only
// manifests. This instance cannot be used for blob checks.
linkPathFns: manifestLinkPathFns,
},
signatures: &signatureStore{
ctx: ctx, ctx: ctx,
repository: repo, repository: repo,
blobStore: repo.blobStore, blobStore: blobStore,
signatures: &signatureStore{
ctx: ctx,
repository: repo,
blobStore: repo.blobStore,
},
},
schema2Handler: &schema2ManifestHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
},
manifestListHandler: &manifestListHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
}, },
} }

View file

@ -0,0 +1,99 @@
package storage
import (
"fmt"
"encoding/json"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/schema2"
)
//schema2ManifestHandler is a ManifestHandler that covers schema2 manifests.
type schema2ManifestHandler struct {
repository *repository
blobStore *linkedBlobStore
ctx context.Context
}
var _ ManifestHandler = &schema2ManifestHandler{}
func (ms *schema2ManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Unmarshal")
var m schema2.DeserializedManifest
if err := json.Unmarshal(content, &m); err != nil {
return nil, err
}
return &m, nil
}
func (ms *schema2ManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Put")
m, ok := manifest.(*schema2.DeserializedManifest)
if !ok {
return "", fmt.Errorf("non-schema2 manifest put to schema2ManifestHandler: %T", manifest)
}
if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil {
return "", err
}
mt, payload, err := m.Payload()
if err != nil {
return "", err
}
revision, err := ms.blobStore.Put(ctx, mt, payload)
if err != nil {
context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
return "", err
}
// Link the revision into the repository.
if err := ms.blobStore.linkBlob(ctx, revision); err != nil {
return "", err
}
return revision.Digest, nil
}
// verifyManifest ensures that the manifest content is valid from the
// perspective of the registry. As a policy, the registry only tries to store
// valid content, leaving trust policies of that content up to consumers.
func (ms *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst schema2.DeserializedManifest, skipDependencyVerification bool) error {
var errs distribution.ErrManifestVerification
if !skipDependencyVerification {
target := mnfst.Target()
_, err := ms.repository.Blobs(ctx).Stat(ctx, target.Digest)
if err != nil {
if err != distribution.ErrBlobUnknown {
errs = append(errs, err)
}
// On error here, we always append unknown blob errors.
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: target.Digest})
}
for _, fsLayer := range mnfst.References() {
_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest)
if err != nil {
if err != distribution.ErrBlobUnknown {
errs = append(errs, err)
}
// On error here, we always append unknown blob errors.
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}

View file

@ -0,0 +1,150 @@
package storage
import (
"encoding/json"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference"
"github.com/docker/libtrust"
)
// signedManifestHandler is a ManifestHandler that covers schema1 manifests. It
// can unmarshal and put schema1 manifests that have been signed by libtrust.
type signedManifestHandler struct {
repository *repository
blobStore *linkedBlobStore
ctx context.Context
signatures *signatureStore
}
var _ ManifestHandler = &signedManifestHandler{}
func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) {
context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal")
// Fetch the signatures for the manifest
signatures, err := ms.signatures.Get(dgst)
if err != nil {
return nil, err
}
jsig, err := libtrust.NewJSONSignature(content, signatures...)
if err != nil {
return nil, err
}
// Extract the pretty JWS
raw, err := jsig.PrettySignature("signatures")
if err != nil {
return nil, err
}
var sm schema1.SignedManifest
if err := json.Unmarshal(raw, &sm); err != nil {
return nil, err
}
return &sm, nil
}
func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) {
context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Put")
sm, ok := manifest.(*schema1.SignedManifest)
if !ok {
return "", fmt.Errorf("non-schema1 manifest put to signedManifestHandler: %T", manifest)
}
if err := ms.verifyManifest(ms.ctx, *sm, skipDependencyVerification); err != nil {
return "", err
}
mt := schema1.MediaTypeManifest
payload := sm.Canonical
revision, err := ms.blobStore.Put(ctx, mt, payload)
if err != nil {
context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
return "", err
}
// Link the revision into the repository.
if err := ms.blobStore.linkBlob(ctx, revision); err != nil {
return "", err
}
// Grab each json signature and store them.
signatures, err := sm.Signatures()
if err != nil {
return "", err
}
if err := ms.signatures.Put(revision.Digest, signatures...); err != nil {
return "", err
}
return revision.Digest, nil
}
// verifyManifest ensures that the manifest content is valid from the
// perspective of the registry. It ensures that the signature is valid for the
// enclosed payload. As a policy, the registry only tries to store valid
// content, leaving trust policies of that content up to consumers.
func (ms *signedManifestHandler) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest, skipDependencyVerification bool) error {
var errs distribution.ErrManifestVerification
if len(mnfst.Name) > reference.NameTotalLengthMax {
errs = append(errs,
distribution.ErrManifestNameInvalid{
Name: mnfst.Name,
Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax),
})
}
if !reference.NameRegexp.MatchString(mnfst.Name) {
errs = append(errs,
distribution.ErrManifestNameInvalid{
Name: mnfst.Name,
Reason: fmt.Errorf("invalid manifest name format"),
})
}
if len(mnfst.History) != len(mnfst.FSLayers) {
errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d",
len(mnfst.History), len(mnfst.FSLayers)))
}
if _, err := schema1.Verify(&mnfst); err != nil {
switch err {
case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey:
errs = append(errs, distribution.ErrManifestUnverified{})
default:
if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust
errs = append(errs, distribution.ErrManifestUnverified{})
} else {
errs = append(errs, err)
}
}
}
if !skipDependencyVerification {
for _, fsLayer := range mnfst.References() {
_, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest)
if err != nil {
if err != distribution.ErrBlobUnknown {
errs = append(errs, err)
}
// On error here, we always append unknown blob errors.
errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest})
}
}
}
if len(errs) != 0 {
return errs
}
return nil
}