forked from TrueCloudLab/distribution
Merge pull request #1281 from aaronlehmann/new-manifest
Implement schema2 manifest formats
This commit is contained in:
commit
c426367881
8 changed files with 1167 additions and 213 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
96
docs/storage/manifestlisthandler.go
Normal file
96
docs/storage/manifestlisthandler.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
99
docs/storage/schema2manifesthandler.go
Normal file
99
docs/storage/schema2manifesthandler.go
Normal 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
|
||||||
|
}
|
150
docs/storage/signedmanifesthandler.go
Normal file
150
docs/storage/signedmanifesthandler.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue