| ... | ... |
@@ -155,7 +155,7 @@ RUN set -x \ |
| 155 | 155 |
# both. This allows integration-cli tests to cover push/pull with both schema1 |
| 156 | 156 |
# and schema2 manifests. |
| 157 | 157 |
ENV REGISTRY_COMMIT_SCHEMA1 ec87e9b6971d831f0eff752ddb54fb64693e51cd |
| 158 |
-ENV REGISTRY_COMMIT a7ae88da459b98b481a245e5b1750134724ac67d |
|
| 158 |
+ENV REGISTRY_COMMIT cb08de17d74bef86ce6c5abe8b240e282f5750be |
|
| 159 | 159 |
RUN set -x \ |
| 160 | 160 |
&& export GOPATH="$(mktemp -d)" \ |
| 161 | 161 |
&& git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \ |
| 162 | 162 |
deleted file mode 100644 |
| ... | ... |
@@ -1,100 +0,0 @@ |
| 1 |
-package metadata |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "encoding/json" |
|
| 5 |
- |
|
| 6 |
- "github.com/docker/distribution/digest" |
|
| 7 |
- "github.com/docker/docker/layer" |
|
| 8 |
-) |
|
| 9 |
- |
|
| 10 |
-// BlobSumService maps layer IDs to a set of known blobsums for |
|
| 11 |
-// the layer. |
|
| 12 |
-type BlobSumService struct {
|
|
| 13 |
- store Store |
|
| 14 |
-} |
|
| 15 |
- |
|
| 16 |
-// maxBlobSums is the number of blobsums to keep per layer DiffID. |
|
| 17 |
-const maxBlobSums = 5 |
|
| 18 |
- |
|
| 19 |
-// NewBlobSumService creates a new blobsum mapping service. |
|
| 20 |
-func NewBlobSumService(store Store) *BlobSumService {
|
|
| 21 |
- return &BlobSumService{
|
|
| 22 |
- store: store, |
|
| 23 |
- } |
|
| 24 |
-} |
|
| 25 |
- |
|
| 26 |
-func (blobserv *BlobSumService) diffIDNamespace() string {
|
|
| 27 |
- return "blobsum-storage" |
|
| 28 |
-} |
|
| 29 |
- |
|
| 30 |
-func (blobserv *BlobSumService) blobSumNamespace() string {
|
|
| 31 |
- return "blobsum-lookup" |
|
| 32 |
-} |
|
| 33 |
- |
|
| 34 |
-func (blobserv *BlobSumService) diffIDKey(diffID layer.DiffID) string {
|
|
| 35 |
- return string(digest.Digest(diffID).Algorithm()) + "/" + digest.Digest(diffID).Hex() |
|
| 36 |
-} |
|
| 37 |
- |
|
| 38 |
-func (blobserv *BlobSumService) blobSumKey(blobsum digest.Digest) string {
|
|
| 39 |
- return string(blobsum.Algorithm()) + "/" + blobsum.Hex() |
|
| 40 |
-} |
|
| 41 |
- |
|
| 42 |
-// GetBlobSums finds the blobsums associated with a layer DiffID. |
|
| 43 |
-func (blobserv *BlobSumService) GetBlobSums(diffID layer.DiffID) ([]digest.Digest, error) {
|
|
| 44 |
- jsonBytes, err := blobserv.store.Get(blobserv.diffIDNamespace(), blobserv.diffIDKey(diffID)) |
|
| 45 |
- if err != nil {
|
|
| 46 |
- return nil, err |
|
| 47 |
- } |
|
| 48 |
- |
|
| 49 |
- var blobsums []digest.Digest |
|
| 50 |
- if err := json.Unmarshal(jsonBytes, &blobsums); err != nil {
|
|
| 51 |
- return nil, err |
|
| 52 |
- } |
|
| 53 |
- |
|
| 54 |
- return blobsums, nil |
|
| 55 |
-} |
|
| 56 |
- |
|
| 57 |
-// GetDiffID finds a layer DiffID from a blobsum hash. |
|
| 58 |
-func (blobserv *BlobSumService) GetDiffID(blobsum digest.Digest) (layer.DiffID, error) {
|
|
| 59 |
- diffIDBytes, err := blobserv.store.Get(blobserv.blobSumNamespace(), blobserv.blobSumKey(blobsum)) |
|
| 60 |
- if err != nil {
|
|
| 61 |
- return layer.DiffID(""), err
|
|
| 62 |
- } |
|
| 63 |
- |
|
| 64 |
- return layer.DiffID(diffIDBytes), nil |
|
| 65 |
-} |
|
| 66 |
- |
|
| 67 |
-// Add associates a blobsum with a layer DiffID. If too many blobsums are |
|
| 68 |
-// present, the oldest one is dropped. |
|
| 69 |
-func (blobserv *BlobSumService) Add(diffID layer.DiffID, blobsum digest.Digest) error {
|
|
| 70 |
- oldBlobSums, err := blobserv.GetBlobSums(diffID) |
|
| 71 |
- if err != nil {
|
|
| 72 |
- oldBlobSums = nil |
|
| 73 |
- } |
|
| 74 |
- newBlobSums := make([]digest.Digest, 0, len(oldBlobSums)+1) |
|
| 75 |
- |
|
| 76 |
- // Copy all other blobsums to new slice |
|
| 77 |
- for _, oldSum := range oldBlobSums {
|
|
| 78 |
- if oldSum != blobsum {
|
|
| 79 |
- newBlobSums = append(newBlobSums, oldSum) |
|
| 80 |
- } |
|
| 81 |
- } |
|
| 82 |
- |
|
| 83 |
- newBlobSums = append(newBlobSums, blobsum) |
|
| 84 |
- |
|
| 85 |
- if len(newBlobSums) > maxBlobSums {
|
|
| 86 |
- newBlobSums = newBlobSums[len(newBlobSums)-maxBlobSums:] |
|
| 87 |
- } |
|
| 88 |
- |
|
| 89 |
- jsonBytes, err := json.Marshal(newBlobSums) |
|
| 90 |
- if err != nil {
|
|
| 91 |
- return err |
|
| 92 |
- } |
|
| 93 |
- |
|
| 94 |
- err = blobserv.store.Set(blobserv.diffIDNamespace(), blobserv.diffIDKey(diffID), jsonBytes) |
|
| 95 |
- if err != nil {
|
|
| 96 |
- return err |
|
| 97 |
- } |
|
| 98 |
- |
|
| 99 |
- return blobserv.store.Set(blobserv.blobSumNamespace(), blobserv.blobSumKey(blobsum), []byte(diffID)) |
|
| 100 |
-} |
| 101 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,105 +0,0 @@ |
| 1 |
-package metadata |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "io/ioutil" |
|
| 5 |
- "os" |
|
| 6 |
- "reflect" |
|
| 7 |
- "testing" |
|
| 8 |
- |
|
| 9 |
- "github.com/docker/distribution/digest" |
|
| 10 |
- "github.com/docker/docker/layer" |
|
| 11 |
-) |
|
| 12 |
- |
|
| 13 |
-func TestBlobSumService(t *testing.T) {
|
|
| 14 |
- tmpDir, err := ioutil.TempDir("", "blobsum-storage-service-test")
|
|
| 15 |
- if err != nil {
|
|
| 16 |
- t.Fatalf("could not create temp dir: %v", err)
|
|
| 17 |
- } |
|
| 18 |
- defer os.RemoveAll(tmpDir) |
|
| 19 |
- |
|
| 20 |
- metadataStore, err := NewFSMetadataStore(tmpDir) |
|
| 21 |
- if err != nil {
|
|
| 22 |
- t.Fatalf("could not create metadata store: %v", err)
|
|
| 23 |
- } |
|
| 24 |
- blobSumService := NewBlobSumService(metadataStore) |
|
| 25 |
- |
|
| 26 |
- testVectors := []struct {
|
|
| 27 |
- diffID layer.DiffID |
|
| 28 |
- blobsums []digest.Digest |
|
| 29 |
- }{
|
|
| 30 |
- {
|
|
| 31 |
- diffID: layer.DiffID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
|
| 32 |
- blobsums: []digest.Digest{
|
|
| 33 |
- digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
|
| 34 |
- }, |
|
| 35 |
- }, |
|
| 36 |
- {
|
|
| 37 |
- diffID: layer.DiffID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
|
|
| 38 |
- blobsums: []digest.Digest{
|
|
| 39 |
- digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
|
| 40 |
- digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e"),
|
|
| 41 |
- }, |
|
| 42 |
- }, |
|
| 43 |
- {
|
|
| 44 |
- diffID: layer.DiffID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"),
|
|
| 45 |
- blobsums: []digest.Digest{
|
|
| 46 |
- digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
|
| 47 |
- digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e"),
|
|
| 48 |
- digest.Digest("sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"),
|
|
| 49 |
- digest.Digest("sha256:8902a7ca89aabbb868835260912159026637634090dd8899eee969523252236e"),
|
|
| 50 |
- digest.Digest("sha256:c84364306344ccc48532c52ff5209236273525231dddaaab53262322352883aa"),
|
|
| 51 |
- digest.Digest("sha256:aa7583bbc87532a8352bbb72520a821b3623523523a8352523a52352aaa888fe"),
|
|
| 52 |
- }, |
|
| 53 |
- }, |
|
| 54 |
- } |
|
| 55 |
- |
|
| 56 |
- // Set some associations |
|
| 57 |
- for _, vec := range testVectors {
|
|
| 58 |
- for _, blobsum := range vec.blobsums {
|
|
| 59 |
- err := blobSumService.Add(vec.diffID, blobsum) |
|
| 60 |
- if err != nil {
|
|
| 61 |
- t.Fatalf("error calling Set: %v", err)
|
|
| 62 |
- } |
|
| 63 |
- } |
|
| 64 |
- } |
|
| 65 |
- |
|
| 66 |
- // Check the correct values are read back |
|
| 67 |
- for _, vec := range testVectors {
|
|
| 68 |
- blobsums, err := blobSumService.GetBlobSums(vec.diffID) |
|
| 69 |
- if err != nil {
|
|
| 70 |
- t.Fatalf("error calling Get: %v", err)
|
|
| 71 |
- } |
|
| 72 |
- expectedBlobsums := len(vec.blobsums) |
|
| 73 |
- if expectedBlobsums > 5 {
|
|
| 74 |
- expectedBlobsums = 5 |
|
| 75 |
- } |
|
| 76 |
- if !reflect.DeepEqual(blobsums, vec.blobsums[len(vec.blobsums)-expectedBlobsums:len(vec.blobsums)]) {
|
|
| 77 |
- t.Fatal("Get returned incorrect layer ID")
|
|
| 78 |
- } |
|
| 79 |
- } |
|
| 80 |
- |
|
| 81 |
- // Test GetBlobSums on a nonexistent entry |
|
| 82 |
- _, err = blobSumService.GetBlobSums(layer.DiffID("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
|
| 83 |
- if err == nil {
|
|
| 84 |
- t.Fatal("expected error looking up nonexistent entry")
|
|
| 85 |
- } |
|
| 86 |
- |
|
| 87 |
- // Test GetDiffID on a nonexistent entry |
|
| 88 |
- _, err = blobSumService.GetDiffID(digest.Digest("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
|
| 89 |
- if err == nil {
|
|
| 90 |
- t.Fatal("expected error looking up nonexistent entry")
|
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- // Overwrite one of the entries and read it back |
|
| 94 |
- err = blobSumService.Add(testVectors[1].diffID, testVectors[0].blobsums[0]) |
|
| 95 |
- if err != nil {
|
|
| 96 |
- t.Fatalf("error calling Add: %v", err)
|
|
| 97 |
- } |
|
| 98 |
- diffID, err := blobSumService.GetDiffID(testVectors[0].blobsums[0]) |
|
| 99 |
- if err != nil {
|
|
| 100 |
- t.Fatalf("error calling GetDiffID: %v", err)
|
|
| 101 |
- } |
|
| 102 |
- if diffID != testVectors[1].diffID {
|
|
| 103 |
- t.Fatal("GetDiffID returned incorrect diffID")
|
|
| 104 |
- } |
|
| 105 |
-} |
| ... | ... |
@@ -15,6 +15,8 @@ type Store interface {
|
| 15 | 15 |
Get(namespace string, key string) ([]byte, error) |
| 16 | 16 |
// Set writes data indexed by namespace and key. |
| 17 | 17 |
Set(namespace, key string, value []byte) error |
| 18 |
+ // Delete removes data indexed by namespace and key. |
|
| 19 |
+ Delete(namespace, key string) error |
|
| 18 | 20 |
} |
| 19 | 21 |
|
| 20 | 22 |
// FSMetadataStore uses the filesystem to associate metadata with layer and |
| ... | ... |
@@ -63,3 +65,13 @@ func (store *FSMetadataStore) Set(namespace, key string, value []byte) error {
|
| 63 | 63 |
} |
| 64 | 64 |
return os.Rename(tempFilePath, path) |
| 65 | 65 |
} |
| 66 |
+ |
|
| 67 |
+// Delete removes data indexed by namespace and key. The data file named after |
|
| 68 |
+// the key, stored in the namespace's directory is deleted. |
|
| 69 |
+func (store *FSMetadataStore) Delete(namespace, key string) error {
|
|
| 70 |
+ store.Lock() |
|
| 71 |
+ defer store.Unlock() |
|
| 72 |
+ |
|
| 73 |
+ path := store.path(namespace, key) |
|
| 74 |
+ return os.Remove(path) |
|
| 75 |
+} |
| 66 | 76 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,137 @@ |
| 0 |
+package metadata |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/docker/distribution/digest" |
|
| 6 |
+ "github.com/docker/docker/layer" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// V2MetadataService maps layer IDs to a set of known metadata for |
|
| 10 |
+// the layer. |
|
| 11 |
+type V2MetadataService struct {
|
|
| 12 |
+ store Store |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+// V2Metadata contains the digest and source repository information for a layer. |
|
| 16 |
+type V2Metadata struct {
|
|
| 17 |
+ Digest digest.Digest |
|
| 18 |
+ SourceRepository string |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// maxMetadata is the number of metadata entries to keep per layer DiffID. |
|
| 22 |
+const maxMetadata = 50 |
|
| 23 |
+ |
|
| 24 |
+// NewV2MetadataService creates a new diff ID to v2 metadata mapping service. |
|
| 25 |
+func NewV2MetadataService(store Store) *V2MetadataService {
|
|
| 26 |
+ return &V2MetadataService{
|
|
| 27 |
+ store: store, |
|
| 28 |
+ } |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func (serv *V2MetadataService) diffIDNamespace() string {
|
|
| 32 |
+ return "v2metadata-by-diffid" |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func (serv *V2MetadataService) digestNamespace() string {
|
|
| 36 |
+ return "diffid-by-digest" |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+func (serv *V2MetadataService) diffIDKey(diffID layer.DiffID) string {
|
|
| 40 |
+ return string(digest.Digest(diffID).Algorithm()) + "/" + digest.Digest(diffID).Hex() |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+func (serv *V2MetadataService) digestKey(dgst digest.Digest) string {
|
|
| 44 |
+ return string(dgst.Algorithm()) + "/" + dgst.Hex() |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+// GetMetadata finds the metadata associated with a layer DiffID. |
|
| 48 |
+func (serv *V2MetadataService) GetMetadata(diffID layer.DiffID) ([]V2Metadata, error) {
|
|
| 49 |
+ jsonBytes, err := serv.store.Get(serv.diffIDNamespace(), serv.diffIDKey(diffID)) |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ return nil, err |
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ var metadata []V2Metadata |
|
| 55 |
+ if err := json.Unmarshal(jsonBytes, &metadata); err != nil {
|
|
| 56 |
+ return nil, err |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ return metadata, nil |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// GetDiffID finds a layer DiffID from a digest. |
|
| 63 |
+func (serv *V2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) {
|
|
| 64 |
+ diffIDBytes, err := serv.store.Get(serv.digestNamespace(), serv.digestKey(dgst)) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return layer.DiffID(""), err
|
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ return layer.DiffID(diffIDBytes), nil |
|
| 70 |
+} |
|
| 71 |
+ |
|
| 72 |
+// Add associates metadata with a layer DiffID. If too many metadata entries are |
|
| 73 |
+// present, the oldest one is dropped. |
|
| 74 |
+func (serv *V2MetadataService) Add(diffID layer.DiffID, metadata V2Metadata) error {
|
|
| 75 |
+ oldMetadata, err := serv.GetMetadata(diffID) |
|
| 76 |
+ if err != nil {
|
|
| 77 |
+ oldMetadata = nil |
|
| 78 |
+ } |
|
| 79 |
+ newMetadata := make([]V2Metadata, 0, len(oldMetadata)+1) |
|
| 80 |
+ |
|
| 81 |
+ // Copy all other metadata to new slice |
|
| 82 |
+ for _, oldMeta := range oldMetadata {
|
|
| 83 |
+ if oldMeta != metadata {
|
|
| 84 |
+ newMetadata = append(newMetadata, oldMeta) |
|
| 85 |
+ } |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ newMetadata = append(newMetadata, metadata) |
|
| 89 |
+ |
|
| 90 |
+ if len(newMetadata) > maxMetadata {
|
|
| 91 |
+ newMetadata = newMetadata[len(newMetadata)-maxMetadata:] |
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+ jsonBytes, err := json.Marshal(newMetadata) |
|
| 95 |
+ if err != nil {
|
|
| 96 |
+ return err |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ err = serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) |
|
| 100 |
+ if err != nil {
|
|
| 101 |
+ return err |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ return serv.store.Set(serv.digestNamespace(), serv.digestKey(metadata.Digest), []byte(diffID)) |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+// Remove unassociates a metadata entry from a layer DiffID. |
|
| 108 |
+func (serv *V2MetadataService) Remove(metadata V2Metadata) error {
|
|
| 109 |
+ diffID, err := serv.GetDiffID(metadata.Digest) |
|
| 110 |
+ if err != nil {
|
|
| 111 |
+ return err |
|
| 112 |
+ } |
|
| 113 |
+ oldMetadata, err := serv.GetMetadata(diffID) |
|
| 114 |
+ if err != nil {
|
|
| 115 |
+ oldMetadata = nil |
|
| 116 |
+ } |
|
| 117 |
+ newMetadata := make([]V2Metadata, 0, len(oldMetadata)) |
|
| 118 |
+ |
|
| 119 |
+ // Copy all other metadata to new slice |
|
| 120 |
+ for _, oldMeta := range oldMetadata {
|
|
| 121 |
+ if oldMeta != metadata {
|
|
| 122 |
+ newMetadata = append(newMetadata, oldMeta) |
|
| 123 |
+ } |
|
| 124 |
+ } |
|
| 125 |
+ |
|
| 126 |
+ if len(newMetadata) == 0 {
|
|
| 127 |
+ return serv.store.Delete(serv.diffIDNamespace(), serv.diffIDKey(diffID)) |
|
| 128 |
+ } |
|
| 129 |
+ |
|
| 130 |
+ jsonBytes, err := json.Marshal(newMetadata) |
|
| 131 |
+ if err != nil {
|
|
| 132 |
+ return err |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ return serv.store.Set(serv.diffIDNamespace(), serv.diffIDKey(diffID), jsonBytes) |
|
| 136 |
+} |
| 0 | 137 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,115 @@ |
| 0 |
+package metadata |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/hex" |
|
| 4 |
+ "io/ioutil" |
|
| 5 |
+ "math/rand" |
|
| 6 |
+ "os" |
|
| 7 |
+ "reflect" |
|
| 8 |
+ "testing" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/distribution/digest" |
|
| 11 |
+ "github.com/docker/docker/layer" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+func TestV2MetadataService(t *testing.T) {
|
|
| 15 |
+ tmpDir, err := ioutil.TempDir("", "blobsum-storage-service-test")
|
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ t.Fatalf("could not create temp dir: %v", err)
|
|
| 18 |
+ } |
|
| 19 |
+ defer os.RemoveAll(tmpDir) |
|
| 20 |
+ |
|
| 21 |
+ metadataStore, err := NewFSMetadataStore(tmpDir) |
|
| 22 |
+ if err != nil {
|
|
| 23 |
+ t.Fatalf("could not create metadata store: %v", err)
|
|
| 24 |
+ } |
|
| 25 |
+ V2MetadataService := NewV2MetadataService(metadataStore) |
|
| 26 |
+ |
|
| 27 |
+ tooManyBlobSums := make([]V2Metadata, 100) |
|
| 28 |
+ for i := range tooManyBlobSums {
|
|
| 29 |
+ randDigest := randomDigest() |
|
| 30 |
+ tooManyBlobSums[i] = V2Metadata{Digest: randDigest}
|
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 33 |
+ testVectors := []struct {
|
|
| 34 |
+ diffID layer.DiffID |
|
| 35 |
+ metadata []V2Metadata |
|
| 36 |
+ }{
|
|
| 37 |
+ {
|
|
| 38 |
+ diffID: layer.DiffID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
|
| 39 |
+ metadata: []V2Metadata{
|
|
| 40 |
+ {Digest: digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937")},
|
|
| 41 |
+ }, |
|
| 42 |
+ }, |
|
| 43 |
+ {
|
|
| 44 |
+ diffID: layer.DiffID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
|
|
| 45 |
+ metadata: []V2Metadata{
|
|
| 46 |
+ {Digest: digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937")},
|
|
| 47 |
+ {Digest: digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e")},
|
|
| 48 |
+ }, |
|
| 49 |
+ }, |
|
| 50 |
+ {
|
|
| 51 |
+ diffID: layer.DiffID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"),
|
|
| 52 |
+ metadata: tooManyBlobSums, |
|
| 53 |
+ }, |
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ // Set some associations |
|
| 57 |
+ for _, vec := range testVectors {
|
|
| 58 |
+ for _, blobsum := range vec.metadata {
|
|
| 59 |
+ err := V2MetadataService.Add(vec.diffID, blobsum) |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ t.Fatalf("error calling Set: %v", err)
|
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ } |
|
| 65 |
+ |
|
| 66 |
+ // Check the correct values are read back |
|
| 67 |
+ for _, vec := range testVectors {
|
|
| 68 |
+ metadata, err := V2MetadataService.GetMetadata(vec.diffID) |
|
| 69 |
+ if err != nil {
|
|
| 70 |
+ t.Fatalf("error calling Get: %v", err)
|
|
| 71 |
+ } |
|
| 72 |
+ expectedMetadataEntries := len(vec.metadata) |
|
| 73 |
+ if expectedMetadataEntries > 50 {
|
|
| 74 |
+ expectedMetadataEntries = 50 |
|
| 75 |
+ } |
|
| 76 |
+ if !reflect.DeepEqual(metadata, vec.metadata[len(vec.metadata)-expectedMetadataEntries:len(vec.metadata)]) {
|
|
| 77 |
+ t.Fatal("Get returned incorrect layer ID")
|
|
| 78 |
+ } |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ // Test GetMetadata on a nonexistent entry |
|
| 82 |
+ _, err = V2MetadataService.GetMetadata(layer.DiffID("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
|
| 83 |
+ if err == nil {
|
|
| 84 |
+ t.Fatal("expected error looking up nonexistent entry")
|
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ // Test GetDiffID on a nonexistent entry |
|
| 88 |
+ _, err = V2MetadataService.GetDiffID(digest.Digest("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
|
| 89 |
+ if err == nil {
|
|
| 90 |
+ t.Fatal("expected error looking up nonexistent entry")
|
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ // Overwrite one of the entries and read it back |
|
| 94 |
+ err = V2MetadataService.Add(testVectors[1].diffID, testVectors[0].metadata[0]) |
|
| 95 |
+ if err != nil {
|
|
| 96 |
+ t.Fatalf("error calling Add: %v", err)
|
|
| 97 |
+ } |
|
| 98 |
+ diffID, err := V2MetadataService.GetDiffID(testVectors[0].metadata[0].Digest) |
|
| 99 |
+ if err != nil {
|
|
| 100 |
+ t.Fatalf("error calling GetDiffID: %v", err)
|
|
| 101 |
+ } |
|
| 102 |
+ if diffID != testVectors[1].diffID {
|
|
| 103 |
+ t.Fatal("GetDiffID returned incorrect diffID")
|
|
| 104 |
+ } |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+func randomDigest() digest.Digest {
|
|
| 108 |
+ b := [32]byte{}
|
|
| 109 |
+ for i := 0; i < len(b); i++ {
|
|
| 110 |
+ b[i] = byte(rand.Intn(256)) |
|
| 111 |
+ } |
|
| 112 |
+ d := hex.EncodeToString(b[:]) |
|
| 113 |
+ return digest.Digest("sha256:" + d)
|
|
| 114 |
+} |
| ... | ... |
@@ -61,10 +61,10 @@ func newPuller(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, |
| 61 | 61 |
switch endpoint.Version {
|
| 62 | 62 |
case registry.APIVersion2: |
| 63 | 63 |
return &v2Puller{
|
| 64 |
- blobSumService: metadata.NewBlobSumService(imagePullConfig.MetadataStore), |
|
| 65 |
- endpoint: endpoint, |
|
| 66 |
- config: imagePullConfig, |
|
| 67 |
- repoInfo: repoInfo, |
|
| 64 |
+ V2MetadataService: metadata.NewV2MetadataService(imagePullConfig.MetadataStore), |
|
| 65 |
+ endpoint: endpoint, |
|
| 66 |
+ config: imagePullConfig, |
|
| 67 |
+ repoInfo: repoInfo, |
|
| 68 | 68 |
}, nil |
| 69 | 69 |
case registry.APIVersion1: |
| 70 | 70 |
return &v1Puller{
|
| ... | ... |
@@ -33,11 +33,11 @@ import ( |
| 33 | 33 |
var errRootFSMismatch = errors.New("layers from manifest don't match image configuration")
|
| 34 | 34 |
|
| 35 | 35 |
type v2Puller struct {
|
| 36 |
- blobSumService *metadata.BlobSumService |
|
| 37 |
- endpoint registry.APIEndpoint |
|
| 38 |
- config *ImagePullConfig |
|
| 39 |
- repoInfo *registry.RepositoryInfo |
|
| 40 |
- repo distribution.Repository |
|
| 36 |
+ V2MetadataService *metadata.V2MetadataService |
|
| 37 |
+ endpoint registry.APIEndpoint |
|
| 38 |
+ config *ImagePullConfig |
|
| 39 |
+ repoInfo *registry.RepositoryInfo |
|
| 40 |
+ repo distribution.Repository |
|
| 41 | 41 |
// confirmedV2 is set to true if we confirm we're talking to a v2 |
| 42 | 42 |
// registry. This is used to limit fallbacks to the v1 protocol. |
| 43 | 43 |
confirmedV2 bool |
| ... | ... |
@@ -110,9 +110,10 @@ func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (e |
| 110 | 110 |
} |
| 111 | 111 |
|
| 112 | 112 |
type v2LayerDescriptor struct {
|
| 113 |
- digest digest.Digest |
|
| 114 |
- repo distribution.Repository |
|
| 115 |
- blobSumService *metadata.BlobSumService |
|
| 113 |
+ digest digest.Digest |
|
| 114 |
+ repoInfo *registry.RepositoryInfo |
|
| 115 |
+ repo distribution.Repository |
|
| 116 |
+ V2MetadataService *metadata.V2MetadataService |
|
| 116 | 117 |
} |
| 117 | 118 |
|
| 118 | 119 |
func (ld *v2LayerDescriptor) Key() string {
|
| ... | ... |
@@ -124,7 +125,7 @@ func (ld *v2LayerDescriptor) ID() string {
|
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 | 126 |
func (ld *v2LayerDescriptor) DiffID() (layer.DiffID, error) {
|
| 127 |
- return ld.blobSumService.GetDiffID(ld.digest) |
|
| 127 |
+ return ld.V2MetadataService.GetDiffID(ld.digest) |
|
| 128 | 128 |
} |
| 129 | 129 |
|
| 130 | 130 |
func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progress.Output) (io.ReadCloser, int64, error) {
|
| ... | ... |
@@ -196,7 +197,7 @@ func (ld *v2LayerDescriptor) Download(ctx context.Context, progressOutput progre |
| 196 | 196 |
|
| 197 | 197 |
func (ld *v2LayerDescriptor) Registered(diffID layer.DiffID) {
|
| 198 | 198 |
// Cache mapping from this layer's DiffID to the blobsum |
| 199 |
- ld.blobSumService.Add(diffID, ld.digest) |
|
| 199 |
+ ld.V2MetadataService.Add(diffID, metadata.V2Metadata{Digest: ld.digest, SourceRepository: ld.repoInfo.FullName()})
|
|
| 200 | 200 |
} |
| 201 | 201 |
|
| 202 | 202 |
func (p *v2Puller) pullV2Tag(ctx context.Context, ref reference.Named) (tagUpdated bool, err error) {
|
| ... | ... |
@@ -333,9 +334,10 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverif |
| 333 | 333 |
} |
| 334 | 334 |
|
| 335 | 335 |
layerDescriptor := &v2LayerDescriptor{
|
| 336 |
- digest: blobSum, |
|
| 337 |
- repo: p.repo, |
|
| 338 |
- blobSumService: p.blobSumService, |
|
| 336 |
+ digest: blobSum, |
|
| 337 |
+ repoInfo: p.repoInfo, |
|
| 338 |
+ repo: p.repo, |
|
| 339 |
+ V2MetadataService: p.V2MetadataService, |
|
| 339 | 340 |
} |
| 340 | 341 |
|
| 341 | 342 |
descriptors = append(descriptors, layerDescriptor) |
| ... | ... |
@@ -398,9 +400,10 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s |
| 398 | 398 |
// to top-most, so that the downloads slice gets ordered correctly. |
| 399 | 399 |
for _, d := range mfst.References() {
|
| 400 | 400 |
layerDescriptor := &v2LayerDescriptor{
|
| 401 |
- digest: d.Digest, |
|
| 402 |
- repo: p.repo, |
|
| 403 |
- blobSumService: p.blobSumService, |
|
| 401 |
+ digest: d.Digest, |
|
| 402 |
+ repo: p.repo, |
|
| 403 |
+ repoInfo: p.repoInfo, |
|
| 404 |
+ V2MetadataService: p.V2MetadataService, |
|
| 404 | 405 |
} |
| 405 | 406 |
|
| 406 | 407 |
descriptors = append(descriptors, layerDescriptor) |
| ... | ... |
@@ -71,11 +71,11 @@ func NewPusher(ref reference.Named, endpoint registry.APIEndpoint, repoInfo *reg |
| 71 | 71 |
switch endpoint.Version {
|
| 72 | 72 |
case registry.APIVersion2: |
| 73 | 73 |
return &v2Pusher{
|
| 74 |
- blobSumService: metadata.NewBlobSumService(imagePushConfig.MetadataStore), |
|
| 75 |
- ref: ref, |
|
| 76 |
- endpoint: endpoint, |
|
| 77 |
- repoInfo: repoInfo, |
|
| 78 |
- config: imagePushConfig, |
|
| 74 |
+ v2MetadataService: metadata.NewV2MetadataService(imagePushConfig.MetadataStore), |
|
| 75 |
+ ref: ref, |
|
| 76 |
+ endpoint: endpoint, |
|
| 77 |
+ repoInfo: repoInfo, |
|
| 78 |
+ config: imagePushConfig, |
|
| 79 | 79 |
}, nil |
| 80 | 80 |
case registry.APIVersion1: |
| 81 | 81 |
return &v1Pusher{
|
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
"github.com/docker/distribution/digest" |
| 12 | 12 |
"github.com/docker/distribution/manifest/schema1" |
| 13 | 13 |
"github.com/docker/distribution/manifest/schema2" |
| 14 |
+ distreference "github.com/docker/distribution/reference" |
|
| 14 | 15 |
"github.com/docker/distribution/registry/client" |
| 15 | 16 |
"github.com/docker/docker/distribution/metadata" |
| 16 | 17 |
"github.com/docker/docker/distribution/xfer" |
| ... | ... |
@@ -34,12 +35,12 @@ type PushResult struct {
|
| 34 | 34 |
} |
| 35 | 35 |
|
| 36 | 36 |
type v2Pusher struct {
|
| 37 |
- blobSumService *metadata.BlobSumService |
|
| 38 |
- ref reference.Named |
|
| 39 |
- endpoint registry.APIEndpoint |
|
| 40 |
- repoInfo *registry.RepositoryInfo |
|
| 41 |
- config *ImagePushConfig |
|
| 42 |
- repo distribution.Repository |
|
| 37 |
+ v2MetadataService *metadata.V2MetadataService |
|
| 38 |
+ ref reference.Named |
|
| 39 |
+ endpoint registry.APIEndpoint |
|
| 40 |
+ repoInfo *registry.RepositoryInfo |
|
| 41 |
+ config *ImagePushConfig |
|
| 42 |
+ repo distribution.Repository |
|
| 43 | 43 |
|
| 44 | 44 |
// pushState is state built by the Download functions. |
| 45 | 45 |
pushState pushState |
| ... | ... |
@@ -130,9 +131,10 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, ima |
| 130 | 130 |
var descriptors []xfer.UploadDescriptor |
| 131 | 131 |
|
| 132 | 132 |
descriptorTemplate := v2PushDescriptor{
|
| 133 |
- blobSumService: p.blobSumService, |
|
| 134 |
- repo: p.repo, |
|
| 135 |
- pushState: &p.pushState, |
|
| 133 |
+ v2MetadataService: p.v2MetadataService, |
|
| 134 |
+ repoInfo: p.repoInfo, |
|
| 135 |
+ repo: p.repo, |
|
| 136 |
+ pushState: &p.pushState, |
|
| 136 | 137 |
} |
| 137 | 138 |
|
| 138 | 139 |
// Loop bounds condition is to avoid pushing the base layer on Windows. |
| ... | ... |
@@ -209,10 +211,11 @@ func manifestFromBuilder(ctx context.Context, builder distribution.ManifestBuild |
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 | 211 |
type v2PushDescriptor struct {
|
| 212 |
- layer layer.Layer |
|
| 213 |
- blobSumService *metadata.BlobSumService |
|
| 214 |
- repo distribution.Repository |
|
| 215 |
- pushState *pushState |
|
| 212 |
+ layer layer.Layer |
|
| 213 |
+ v2MetadataService *metadata.V2MetadataService |
|
| 214 |
+ repoInfo reference.Named |
|
| 215 |
+ repo distribution.Repository |
|
| 216 |
+ pushState *pushState |
|
| 216 | 217 |
} |
| 217 | 218 |
|
| 218 | 219 |
func (pd *v2PushDescriptor) Key() string {
|
| ... | ... |
@@ -240,10 +243,10 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
| 240 | 240 |
} |
| 241 | 241 |
pd.pushState.Unlock() |
| 242 | 242 |
|
| 243 |
- // Do we have any blobsums associated with this layer's DiffID? |
|
| 244 |
- possibleBlobsums, err := pd.blobSumService.GetBlobSums(diffID) |
|
| 243 |
+ // Do we have any metadata associated with this layer's DiffID? |
|
| 244 |
+ v2Metadata, err := pd.v2MetadataService.GetMetadata(diffID) |
|
| 245 | 245 |
if err == nil {
|
| 246 |
- descriptor, exists, err := blobSumAlreadyExists(ctx, possibleBlobsums, pd.repo, pd.pushState) |
|
| 246 |
+ descriptor, exists, err := layerAlreadyExists(ctx, v2Metadata, pd.repoInfo, pd.repo, pd.pushState) |
|
| 247 | 247 |
if err != nil {
|
| 248 | 248 |
progress.Update(progressOutput, pd.ID(), "Image push failed") |
| 249 | 249 |
return retryOnError(err) |
| ... | ... |
@@ -263,8 +266,69 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
| 263 | 263 |
// then push the blob. |
| 264 | 264 |
bs := pd.repo.Blobs(ctx) |
| 265 | 265 |
|
| 266 |
+ var mountFrom metadata.V2Metadata |
|
| 267 |
+ |
|
| 268 |
+ // Attempt to find another repository in the same registry to mount the layer from to avoid an unnecessary upload |
|
| 269 |
+ for _, metadata := range v2Metadata {
|
|
| 270 |
+ sourceRepo, err := reference.ParseNamed(metadata.SourceRepository) |
|
| 271 |
+ if err != nil {
|
|
| 272 |
+ continue |
|
| 273 |
+ } |
|
| 274 |
+ if pd.repoInfo.Hostname() == sourceRepo.Hostname() {
|
|
| 275 |
+ logrus.Debugf("attempting to mount layer %s (%s) from %s", diffID, metadata.Digest, sourceRepo.FullName())
|
|
| 276 |
+ mountFrom = metadata |
|
| 277 |
+ break |
|
| 278 |
+ } |
|
| 279 |
+ } |
|
| 280 |
+ |
|
| 281 |
+ var createOpts []distribution.BlobCreateOption |
|
| 282 |
+ |
|
| 283 |
+ if mountFrom.SourceRepository != "" {
|
|
| 284 |
+ namedRef, err := reference.WithName(mountFrom.SourceRepository) |
|
| 285 |
+ if err != nil {
|
|
| 286 |
+ return err |
|
| 287 |
+ } |
|
| 288 |
+ |
|
| 289 |
+ // TODO (brianbland): We need to construct a reference where the Name is |
|
| 290 |
+ // only the full remote name, so clean this up when distribution has a |
|
| 291 |
+ // richer reference package |
|
| 292 |
+ remoteRef, err := distreference.WithName(namedRef.RemoteName()) |
|
| 293 |
+ if err != nil {
|
|
| 294 |
+ return err |
|
| 295 |
+ } |
|
| 296 |
+ |
|
| 297 |
+ canonicalRef, err := distreference.WithDigest(remoteRef, mountFrom.Digest) |
|
| 298 |
+ if err != nil {
|
|
| 299 |
+ return err |
|
| 300 |
+ } |
|
| 301 |
+ |
|
| 302 |
+ createOpts = append(createOpts, client.WithMountFrom(canonicalRef)) |
|
| 303 |
+ } |
|
| 304 |
+ |
|
| 266 | 305 |
// Send the layer |
| 267 |
- layerUpload, err := bs.Create(ctx) |
|
| 306 |
+ layerUpload, err := bs.Create(ctx, createOpts...) |
|
| 307 |
+ switch err := err.(type) {
|
|
| 308 |
+ case distribution.ErrBlobMounted: |
|
| 309 |
+ progress.Updatef(progressOutput, pd.ID(), "Mounted from %s", err.From.Name()) |
|
| 310 |
+ |
|
| 311 |
+ pd.pushState.Lock() |
|
| 312 |
+ pd.pushState.confirmedV2 = true |
|
| 313 |
+ pd.pushState.remoteLayers[diffID] = err.Descriptor |
|
| 314 |
+ pd.pushState.Unlock() |
|
| 315 |
+ |
|
| 316 |
+ // Cache mapping from this layer's DiffID to the blobsum |
|
| 317 |
+ if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: mountFrom.Digest, SourceRepository: pd.repoInfo.FullName()}); err != nil {
|
|
| 318 |
+ return xfer.DoNotRetry{Err: err}
|
|
| 319 |
+ } |
|
| 320 |
+ |
|
| 321 |
+ return nil |
|
| 322 |
+ } |
|
| 323 |
+ if mountFrom.SourceRepository != "" {
|
|
| 324 |
+ // unable to mount layer from this repository, so this source mapping is no longer valid |
|
| 325 |
+ logrus.Debugf("unassociating layer %s (%s) with %s", diffID, mountFrom.Digest, mountFrom.SourceRepository)
|
|
| 326 |
+ pd.v2MetadataService.Remove(mountFrom) |
|
| 327 |
+ } |
|
| 328 |
+ |
|
| 268 | 329 |
if err != nil {
|
| 269 | 330 |
return retryOnError(err) |
| 270 | 331 |
} |
| ... | ... |
@@ -300,7 +364,7 @@ func (pd *v2PushDescriptor) Upload(ctx context.Context, progressOutput progress. |
| 300 | 300 |
progress.Update(progressOutput, pd.ID(), "Pushed") |
| 301 | 301 |
|
| 302 | 302 |
// Cache mapping from this layer's DiffID to the blobsum |
| 303 |
- if err := pd.blobSumService.Add(diffID, pushDigest); err != nil {
|
|
| 303 |
+ if err := pd.v2MetadataService.Add(diffID, metadata.V2Metadata{Digest: pushDigest, SourceRepository: pd.repoInfo.FullName()}); err != nil {
|
|
| 304 | 304 |
return xfer.DoNotRetry{Err: err}
|
| 305 | 305 |
} |
| 306 | 306 |
|
| ... | ... |
@@ -329,12 +393,16 @@ func (pd *v2PushDescriptor) Descriptor() distribution.Descriptor {
|
| 329 | 329 |
return pd.pushState.remoteLayers[pd.DiffID()] |
| 330 | 330 |
} |
| 331 | 331 |
|
| 332 |
-// blobSumAlreadyExists checks if the registry already know about any of the |
|
| 333 |
-// blobsums passed in the "blobsums" slice. If it finds one that the registry |
|
| 332 |
+// layerAlreadyExists checks if the registry already know about any of the |
|
| 333 |
+// metadata passed in the "metadata" slice. If it finds one that the registry |
|
| 334 | 334 |
// knows about, it returns the known digest and "true". |
| 335 |
-func blobSumAlreadyExists(ctx context.Context, blobsums []digest.Digest, repo distribution.Repository, pushState *pushState) (distribution.Descriptor, bool, error) {
|
|
| 336 |
- for _, dgst := range blobsums {
|
|
| 337 |
- descriptor, err := repo.Blobs(ctx).Stat(ctx, dgst) |
|
| 335 |
+func layerAlreadyExists(ctx context.Context, metadata []metadata.V2Metadata, repoInfo reference.Named, repo distribution.Repository, pushState *pushState) (distribution.Descriptor, bool, error) {
|
|
| 336 |
+ for _, meta := range metadata {
|
|
| 337 |
+ // Only check blobsums that are known to this repository or have an unknown source |
|
| 338 |
+ if meta.SourceRepository != "" && meta.SourceRepository != repoInfo.FullName() {
|
|
| 339 |
+ continue |
|
| 340 |
+ } |
|
| 341 |
+ descriptor, err := repo.Blobs(ctx).Stat(ctx, meta.Digest) |
|
| 338 | 342 |
switch err {
|
| 339 | 343 |
case nil: |
| 340 | 344 |
descriptor.MediaType = schema2.MediaTypeLayer |
| ... | ... |
@@ -45,7 +45,7 @@ clone git github.com/boltdb/bolt v1.1.0 |
| 45 | 45 |
clone git github.com/miekg/dns d27455715200c7d3e321a1e5cadb27c9ee0b0f02 |
| 46 | 46 |
|
| 47 | 47 |
# get graph and distribution packages |
| 48 |
-clone git github.com/docker/distribution a7ae88da459b98b481a245e5b1750134724ac67d |
|
| 48 |
+clone git github.com/docker/distribution cb08de17d74bef86ce6c5abe8b240e282f5750be |
|
| 49 | 49 |
clone git github.com/vbatts/tar-split v0.9.11 |
| 50 | 50 |
|
| 51 | 51 |
# get desired notary commit, might also need to be updated in Dockerfile |
| ... | ... |
@@ -147,6 +147,58 @@ func (s *DockerSchema1RegistrySuite) TestPushEmptyLayer(c *check.C) {
|
| 147 | 147 |
testPushEmptyLayer(c) |
| 148 | 148 |
} |
| 149 | 149 |
|
| 150 |
+func (s *DockerRegistrySuite) TestCrossRepositoryLayerPush(c *check.C) {
|
|
| 151 |
+ sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
|
| 152 |
+ // tag the image to upload it to the private registry |
|
| 153 |
+ dockerCmd(c, "tag", "busybox", sourceRepoName) |
|
| 154 |
+ // push the image to the registry |
|
| 155 |
+ out1, _, err := dockerCmdWithError("push", sourceRepoName)
|
|
| 156 |
+ c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out1))
|
|
| 157 |
+ // ensure that none of the layers were mounted from another repository during push |
|
| 158 |
+ c.Assert(strings.Contains(out1, "Mounted from"), check.Equals, false) |
|
| 159 |
+ |
|
| 160 |
+ destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL)
|
|
| 161 |
+ // retag the image to upload the same layers to another repo in the same registry |
|
| 162 |
+ dockerCmd(c, "tag", "busybox", destRepoName) |
|
| 163 |
+ // push the image to the registry |
|
| 164 |
+ out2, _, err := dockerCmdWithError("push", destRepoName)
|
|
| 165 |
+ c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out2))
|
|
| 166 |
+ // ensure that layers were mounted from the first repo during push |
|
| 167 |
+ c.Assert(strings.Contains(out2, "Mounted from dockercli/busybox"), check.Equals, true) |
|
| 168 |
+ |
|
| 169 |
+ // ensure that we can pull and run the cross-repo-pushed repository |
|
| 170 |
+ dockerCmd(c, "rmi", destRepoName) |
|
| 171 |
+ dockerCmd(c, "pull", destRepoName) |
|
| 172 |
+ out3, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") |
|
| 173 |
+ c.Assert(out3, check.Equals, "hello world") |
|
| 174 |
+} |
|
| 175 |
+ |
|
| 176 |
+func (s *DockerSchema1RegistrySuite) TestCrossRepositoryLayerPushNotSupported(c *check.C) {
|
|
| 177 |
+ sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
|
|
| 178 |
+ // tag the image to upload it to the private registry |
|
| 179 |
+ dockerCmd(c, "tag", "busybox", sourceRepoName) |
|
| 180 |
+ // push the image to the registry |
|
| 181 |
+ out1, _, err := dockerCmdWithError("push", sourceRepoName)
|
|
| 182 |
+ c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out1))
|
|
| 183 |
+ // ensure that none of the layers were mounted from another repository during push |
|
| 184 |
+ c.Assert(strings.Contains(out1, "Mounted from"), check.Equals, false) |
|
| 185 |
+ |
|
| 186 |
+ destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL)
|
|
| 187 |
+ // retag the image to upload the same layers to another repo in the same registry |
|
| 188 |
+ dockerCmd(c, "tag", "busybox", destRepoName) |
|
| 189 |
+ // push the image to the registry |
|
| 190 |
+ out2, _, err := dockerCmdWithError("push", destRepoName)
|
|
| 191 |
+ c.Assert(err, check.IsNil, check.Commentf("pushing the image to the private registry has failed: %s", out2))
|
|
| 192 |
+ // schema1 registry should not support cross-repo layer mounts, so ensure that this does not happen |
|
| 193 |
+ c.Assert(strings.Contains(out2, "Mounted from dockercli/busybox"), check.Equals, false) |
|
| 194 |
+ |
|
| 195 |
+ // ensure that we can pull and run the second pushed repository |
|
| 196 |
+ dockerCmd(c, "rmi", destRepoName) |
|
| 197 |
+ dockerCmd(c, "pull", destRepoName) |
|
| 198 |
+ out3, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") |
|
| 199 |
+ c.Assert(out3, check.Equals, "hello world") |
|
| 200 |
+} |
|
| 201 |
+ |
|
| 150 | 202 |
func (s *DockerTrustSuite) TestTrustedPush(c *check.C) {
|
| 151 | 203 |
repoName := fmt.Sprintf("%v/dockercli/trusted:latest", privateRegistryURL)
|
| 152 | 204 |
// tag the image and upload it to the private registry |
| ... | ... |
@@ -476,8 +476,8 @@ func migrateImage(id, root string, ls graphIDRegistrar, is image.Store, ms metad |
| 476 | 476 |
if err == nil { // best effort
|
| 477 | 477 |
dgst, err := digest.ParseDigest(string(checksum)) |
| 478 | 478 |
if err == nil {
|
| 479 |
- blobSumService := metadata.NewBlobSumService(ms) |
|
| 480 |
- blobSumService.Add(layer.DiffID(), dgst) |
|
| 479 |
+ V2MetadataService := metadata.NewV2MetadataService(ms) |
|
| 480 |
+ V2MetadataService.Add(layer.DiffID(), metadata.V2Metadata{Digest: dgst})
|
|
| 481 | 481 |
} |
| 482 | 482 |
} |
| 483 | 483 |
_, err = ls.Release(layer) |
| ... | ... |
@@ -210,19 +210,19 @@ func TestMigrateImages(t *testing.T) {
|
| 210 | 210 |
t.Fatalf("invalid register count: expected %q, got %q", expected, actual)
|
| 211 | 211 |
} |
| 212 | 212 |
|
| 213 |
- blobSumService := metadata.NewBlobSumService(ms) |
|
| 214 |
- blobsums, err := blobSumService.GetBlobSums(layer.EmptyLayer.DiffID()) |
|
| 213 |
+ v2MetadataService := metadata.NewV2MetadataService(ms) |
|
| 214 |
+ receivedMetadata, err := v2MetadataService.GetMetadata(layer.EmptyLayer.DiffID()) |
|
| 215 | 215 |
if err != nil {
|
| 216 | 216 |
t.Fatal(err) |
| 217 | 217 |
} |
| 218 | 218 |
|
| 219 |
- expectedBlobsums := []digest.Digest{
|
|
| 220 |
- "sha256:55dc925c23d1ed82551fd018c27ac3ee731377b6bad3963a2a4e76e753d70e57", |
|
| 221 |
- "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", |
|
| 219 |
+ expectedMetadata := []metadata.V2Metadata{
|
|
| 220 |
+ {Digest: digest.Digest("sha256:55dc925c23d1ed82551fd018c27ac3ee731377b6bad3963a2a4e76e753d70e57")},
|
|
| 221 |
+ {Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
|
| 222 | 222 |
} |
| 223 | 223 |
|
| 224 |
- if !reflect.DeepEqual(expectedBlobsums, blobsums) {
|
|
| 225 |
- t.Fatalf("invalid blobsums: expected %q, got %q", expectedBlobsums, blobsums)
|
|
| 224 |
+ if !reflect.DeepEqual(expectedMetadata, receivedMetadata) {
|
|
| 225 |
+ t.Fatalf("invalid metadata: expected %q, got %q", expectedMetadata, receivedMetadata)
|
|
| 226 | 226 |
} |
| 227 | 227 |
|
| 228 | 228 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
|
| 10 | 10 |
"github.com/docker/distribution/context" |
| 11 | 11 |
"github.com/docker/distribution/digest" |
| 12 |
+ "github.com/docker/distribution/reference" |
|
| 12 | 13 |
) |
| 13 | 14 |
|
| 14 | 15 |
var ( |
| ... | ... |
@@ -40,6 +41,18 @@ func (err ErrBlobInvalidDigest) Error() string {
|
| 40 | 40 |
err.Digest, err.Reason) |
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 |
+// ErrBlobMounted returned when a blob is mounted from another repository |
|
| 44 |
+// instead of initiating an upload session. |
|
| 45 |
+type ErrBlobMounted struct {
|
|
| 46 |
+ From reference.Canonical |
|
| 47 |
+ Descriptor Descriptor |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+func (err ErrBlobMounted) Error() string {
|
|
| 51 |
+ return fmt.Sprintf("blob mounted from: %v to: %v",
|
|
| 52 |
+ err.From, err.Descriptor) |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 43 | 55 |
// Descriptor describes targeted content. Used in conjunction with a blob |
| 44 | 56 |
// store, a descriptor can be used to fetch, store and target any kind of |
| 45 | 57 |
// blob. The struct also describes the wire protocol format. Fields should |
| ... | ... |
@@ -151,12 +164,21 @@ type BlobIngester interface {
|
| 151 | 151 |
// returned handle can be written to and later resumed using an opaque |
| 152 | 152 |
// identifier. With this approach, one can Close and Resume a BlobWriter |
| 153 | 153 |
// multiple times until the BlobWriter is committed or cancelled. |
| 154 |
- Create(ctx context.Context) (BlobWriter, error) |
|
| 154 |
+ Create(ctx context.Context, options ...BlobCreateOption) (BlobWriter, error) |
|
| 155 | 155 |
|
| 156 | 156 |
// Resume attempts to resume a write to a blob, identified by an id. |
| 157 | 157 |
Resume(ctx context.Context, id string) (BlobWriter, error) |
| 158 | 158 |
} |
| 159 | 159 |
|
| 160 |
+// BlobCreateOption is a general extensible function argument for blob creation |
|
| 161 |
+// methods. A BlobIngester may choose to honor any or none of the given |
|
| 162 |
+// BlobCreateOptions, which can be specific to the implementation of the |
|
| 163 |
+// BlobIngester receiving them. |
|
| 164 |
+// TODO (brianbland): unify this with ManifestServiceOption in the future |
|
| 165 |
+type BlobCreateOption interface {
|
|
| 166 |
+ Apply(interface{}) error
|
|
| 167 |
+} |
|
| 168 |
+ |
|
| 160 | 169 |
// BlobWriter provides a handle for inserting data into a blob store. |
| 161 | 170 |
// Instances should be obtained from BlobWriteService.Writer and |
| 162 | 171 |
// BlobWriteService.Resume. If supported by the store, a writer can be |
| ... | ... |
@@ -1041,6 +1041,70 @@ var routeDescriptors = []RouteDescriptor{
|
| 1041 | 1041 |
deniedResponseDescriptor, |
| 1042 | 1042 |
}, |
| 1043 | 1043 |
}, |
| 1044 |
+ {
|
|
| 1045 |
+ Name: "Mount Blob", |
|
| 1046 |
+ Description: "Mount a blob identified by the `mount` parameter from another repository.", |
|
| 1047 |
+ Headers: []ParameterDescriptor{
|
|
| 1048 |
+ hostHeader, |
|
| 1049 |
+ authHeader, |
|
| 1050 |
+ contentLengthZeroHeader, |
|
| 1051 |
+ }, |
|
| 1052 |
+ PathParameters: []ParameterDescriptor{
|
|
| 1053 |
+ nameParameterDescriptor, |
|
| 1054 |
+ }, |
|
| 1055 |
+ QueryParameters: []ParameterDescriptor{
|
|
| 1056 |
+ {
|
|
| 1057 |
+ Name: "mount", |
|
| 1058 |
+ Type: "query", |
|
| 1059 |
+ Format: "<digest>", |
|
| 1060 |
+ Regexp: digest.DigestRegexp, |
|
| 1061 |
+ Description: `Digest of blob to mount from the source repository.`, |
|
| 1062 |
+ }, |
|
| 1063 |
+ {
|
|
| 1064 |
+ Name: "from", |
|
| 1065 |
+ Type: "query", |
|
| 1066 |
+ Format: "<repository name>", |
|
| 1067 |
+ Regexp: reference.NameRegexp, |
|
| 1068 |
+ Description: `Name of the source repository.`, |
|
| 1069 |
+ }, |
|
| 1070 |
+ }, |
|
| 1071 |
+ Successes: []ResponseDescriptor{
|
|
| 1072 |
+ {
|
|
| 1073 |
+ Description: "The blob has been mounted in the repository and is available at the provided location.", |
|
| 1074 |
+ StatusCode: http.StatusCreated, |
|
| 1075 |
+ Headers: []ParameterDescriptor{
|
|
| 1076 |
+ {
|
|
| 1077 |
+ Name: "Location", |
|
| 1078 |
+ Type: "url", |
|
| 1079 |
+ Format: "<blob location>", |
|
| 1080 |
+ }, |
|
| 1081 |
+ contentLengthZeroHeader, |
|
| 1082 |
+ dockerUploadUUIDHeader, |
|
| 1083 |
+ }, |
|
| 1084 |
+ }, |
|
| 1085 |
+ }, |
|
| 1086 |
+ Failures: []ResponseDescriptor{
|
|
| 1087 |
+ {
|
|
| 1088 |
+ Name: "Invalid Name or Digest", |
|
| 1089 |
+ StatusCode: http.StatusBadRequest, |
|
| 1090 |
+ ErrorCodes: []errcode.ErrorCode{
|
|
| 1091 |
+ ErrorCodeDigestInvalid, |
|
| 1092 |
+ ErrorCodeNameInvalid, |
|
| 1093 |
+ }, |
|
| 1094 |
+ }, |
|
| 1095 |
+ {
|
|
| 1096 |
+ Name: "Not allowed", |
|
| 1097 |
+ Description: "Blob mount is not allowed because the registry is configured as a pull-through cache or for some other reason", |
|
| 1098 |
+ StatusCode: http.StatusMethodNotAllowed, |
|
| 1099 |
+ ErrorCodes: []errcode.ErrorCode{
|
|
| 1100 |
+ errcode.ErrorCodeUnsupported, |
|
| 1101 |
+ }, |
|
| 1102 |
+ }, |
|
| 1103 |
+ unauthorizedResponseDescriptor, |
|
| 1104 |
+ repositoryNotFoundResponseDescriptor, |
|
| 1105 |
+ deniedResponseDescriptor, |
|
| 1106 |
+ }, |
|
| 1107 |
+ }, |
|
| 1044 | 1108 |
}, |
| 1045 | 1109 |
}, |
| 1046 | 1110 |
}, |
| ... | ... |
@@ -108,6 +108,8 @@ type tokenHandler struct {
|
| 108 | 108 |
tokenLock sync.Mutex |
| 109 | 109 |
tokenCache string |
| 110 | 110 |
tokenExpiration time.Time |
| 111 |
+ |
|
| 112 |
+ additionalScopes map[string]struct{}
|
|
| 111 | 113 |
} |
| 112 | 114 |
|
| 113 | 115 |
// tokenScope represents the scope at which a token will be requested. |
| ... | ... |
@@ -145,6 +147,7 @@ func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock |
| 145 | 145 |
Scope: scope, |
| 146 | 146 |
Actions: actions, |
| 147 | 147 |
}, |
| 148 |
+ additionalScopes: map[string]struct{}{},
|
|
| 148 | 149 |
} |
| 149 | 150 |
} |
| 150 | 151 |
|
| ... | ... |
@@ -160,7 +163,15 @@ func (th *tokenHandler) Scheme() string {
|
| 160 | 160 |
} |
| 161 | 161 |
|
| 162 | 162 |
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
| 163 |
- if err := th.refreshToken(params); err != nil {
|
|
| 163 |
+ var additionalScopes []string |
|
| 164 |
+ if fromParam := req.URL.Query().Get("from"); fromParam != "" {
|
|
| 165 |
+ additionalScopes = append(additionalScopes, tokenScope{
|
|
| 166 |
+ Resource: "repository", |
|
| 167 |
+ Scope: fromParam, |
|
| 168 |
+ Actions: []string{"pull"},
|
|
| 169 |
+ }.String()) |
|
| 170 |
+ } |
|
| 171 |
+ if err := th.refreshToken(params, additionalScopes...); err != nil {
|
|
| 164 | 172 |
return err |
| 165 | 173 |
} |
| 166 | 174 |
|
| ... | ... |
@@ -169,11 +180,18 @@ func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]st |
| 169 | 169 |
return nil |
| 170 | 170 |
} |
| 171 | 171 |
|
| 172 |
-func (th *tokenHandler) refreshToken(params map[string]string) error {
|
|
| 172 |
+func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
|
|
| 173 | 173 |
th.tokenLock.Lock() |
| 174 | 174 |
defer th.tokenLock.Unlock() |
| 175 |
+ var addedScopes bool |
|
| 176 |
+ for _, scope := range additionalScopes {
|
|
| 177 |
+ if _, ok := th.additionalScopes[scope]; !ok {
|
|
| 178 |
+ th.additionalScopes[scope] = struct{}{}
|
|
| 179 |
+ addedScopes = true |
|
| 180 |
+ } |
|
| 181 |
+ } |
|
| 175 | 182 |
now := th.clock.Now() |
| 176 |
- if now.After(th.tokenExpiration) {
|
|
| 183 |
+ if now.After(th.tokenExpiration) || addedScopes {
|
|
| 177 | 184 |
tr, err := th.fetchToken(params) |
| 178 | 185 |
if err != nil {
|
| 179 | 186 |
return err |
| ... | ... |
@@ -223,6 +241,10 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon |
| 223 | 223 |
reqParams.Add("scope", scopeField)
|
| 224 | 224 |
} |
| 225 | 225 |
|
| 226 |
+ for scope := range th.additionalScopes {
|
|
| 227 |
+ reqParams.Add("scope", scope)
|
|
| 228 |
+ } |
|
| 229 |
+ |
|
| 226 | 230 |
if th.creds != nil {
|
| 227 | 231 |
username, password := th.creds.Basic(realmURL) |
| 228 | 232 |
if username != "" && password != "" {
|
| ... | ... |
@@ -572,8 +572,57 @@ func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribut |
| 572 | 572 |
return writer.Commit(ctx, desc) |
| 573 | 573 |
} |
| 574 | 574 |
|
| 575 |
-func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
|
| 576 |
- u, err := bs.ub.BuildBlobUploadURL(bs.name) |
|
| 575 |
+// createOptions is a collection of blob creation modifiers relevant to general |
|
| 576 |
+// blob storage intended to be configured by the BlobCreateOption.Apply method. |
|
| 577 |
+type createOptions struct {
|
|
| 578 |
+ Mount struct {
|
|
| 579 |
+ ShouldMount bool |
|
| 580 |
+ From reference.Canonical |
|
| 581 |
+ } |
|
| 582 |
+} |
|
| 583 |
+ |
|
| 584 |
+type optionFunc func(interface{}) error
|
|
| 585 |
+ |
|
| 586 |
+func (f optionFunc) Apply(v interface{}) error {
|
|
| 587 |
+ return f(v) |
|
| 588 |
+} |
|
| 589 |
+ |
|
| 590 |
+// WithMountFrom returns a BlobCreateOption which designates that the blob should be |
|
| 591 |
+// mounted from the given canonical reference. |
|
| 592 |
+func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption {
|
|
| 593 |
+ return optionFunc(func(v interface{}) error {
|
|
| 594 |
+ opts, ok := v.(*createOptions) |
|
| 595 |
+ if !ok {
|
|
| 596 |
+ return fmt.Errorf("unexpected options type: %T", v)
|
|
| 597 |
+ } |
|
| 598 |
+ |
|
| 599 |
+ opts.Mount.ShouldMount = true |
|
| 600 |
+ opts.Mount.From = ref |
|
| 601 |
+ |
|
| 602 |
+ return nil |
|
| 603 |
+ }) |
|
| 604 |
+} |
|
| 605 |
+ |
|
| 606 |
+func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
|
|
| 607 |
+ var opts createOptions |
|
| 608 |
+ |
|
| 609 |
+ for _, option := range options {
|
|
| 610 |
+ err := option.Apply(&opts) |
|
| 611 |
+ if err != nil {
|
|
| 612 |
+ return nil, err |
|
| 613 |
+ } |
|
| 614 |
+ } |
|
| 615 |
+ |
|
| 616 |
+ var values []url.Values |
|
| 617 |
+ |
|
| 618 |
+ if opts.Mount.ShouldMount {
|
|
| 619 |
+ values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}})
|
|
| 620 |
+ } |
|
| 621 |
+ |
|
| 622 |
+ u, err := bs.ub.BuildBlobUploadURL(bs.name, values...) |
|
| 623 |
+ if err != nil {
|
|
| 624 |
+ return nil, err |
|
| 625 |
+ } |
|
| 577 | 626 |
|
| 578 | 627 |
resp, err := bs.client.Post(u, "", nil) |
| 579 | 628 |
if err != nil {
|
| ... | ... |
@@ -581,7 +630,14 @@ func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
| 581 | 581 |
} |
| 582 | 582 |
defer resp.Body.Close() |
| 583 | 583 |
|
| 584 |
- if SuccessStatus(resp.StatusCode) {
|
|
| 584 |
+ switch resp.StatusCode {
|
|
| 585 |
+ case http.StatusCreated: |
|
| 586 |
+ desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest()) |
|
| 587 |
+ if err != nil {
|
|
| 588 |
+ return nil, err |
|
| 589 |
+ } |
|
| 590 |
+ return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc}
|
|
| 591 |
+ case http.StatusAccepted: |
|
| 585 | 592 |
// TODO(dmcgowan): Check for invalid UUID |
| 586 | 593 |
uuid := resp.Header.Get("Docker-Upload-UUID")
|
| 587 | 594 |
location, err := sanitizeLocation(resp.Header.Get("Location"), u)
|
| ... | ... |
@@ -596,8 +652,9 @@ func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
| 596 | 596 |
startedAt: time.Now(), |
| 597 | 597 |
location: location, |
| 598 | 598 |
}, nil |
| 599 |
+ default: |
|
| 600 |
+ return nil, HandleErrorResponse(resp) |
|
| 599 | 601 |
} |
| 600 |
- return nil, HandleErrorResponse(resp) |
|
| 601 | 602 |
} |
| 602 | 603 |
|
| 603 | 604 |
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
|