Tizen_4.0 base
[platform/upstream/docker-engine.git] / distribution / push_v2_test.go
1 package distribution
2
3 import (
4         "net/http"
5         "reflect"
6         "testing"
7
8         "github.com/docker/distribution"
9         "github.com/docker/distribution/context"
10         "github.com/docker/distribution/manifest/schema2"
11         "github.com/docker/distribution/reference"
12         "github.com/docker/docker/distribution/metadata"
13         "github.com/docker/docker/layer"
14         "github.com/docker/docker/pkg/progress"
15         "github.com/opencontainers/go-digest"
16 )
17
18 func TestGetRepositoryMountCandidates(t *testing.T) {
19         for _, tc := range []struct {
20                 name          string
21                 hmacKey       string
22                 targetRepo    string
23                 maxCandidates int
24                 metadata      []metadata.V2Metadata
25                 candidates    []metadata.V2Metadata
26         }{
27                 {
28                         name:          "empty metadata",
29                         targetRepo:    "busybox",
30                         maxCandidates: -1,
31                         metadata:      []metadata.V2Metadata{},
32                         candidates:    []metadata.V2Metadata{},
33                 },
34                 {
35                         name:          "one item not matching",
36                         targetRepo:    "busybox",
37                         maxCandidates: -1,
38                         metadata:      []metadata.V2Metadata{taggedMetadata("key", "dgst", "127.0.0.1/repo")},
39                         candidates:    []metadata.V2Metadata{},
40                 },
41                 {
42                         name:          "one item matching",
43                         targetRepo:    "busybox",
44                         maxCandidates: -1,
45                         metadata:      []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
46                         candidates:    []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
47                 },
48                 {
49                         name:          "allow missing SourceRepository",
50                         targetRepo:    "busybox",
51                         maxCandidates: -1,
52                         metadata: []metadata.V2Metadata{
53                                 {Digest: digest.Digest("1")},
54                                 {Digest: digest.Digest("3")},
55                                 {Digest: digest.Digest("2")},
56                         },
57                         candidates: []metadata.V2Metadata{},
58                 },
59                 {
60                         name:          "handle docker.io",
61                         targetRepo:    "user/app",
62                         maxCandidates: -1,
63                         metadata: []metadata.V2Metadata{
64                                 {Digest: digest.Digest("1"), SourceRepository: "docker.io/user/foo"},
65                                 {Digest: digest.Digest("3"), SourceRepository: "docker.io/user/bar"},
66                                 {Digest: digest.Digest("2"), SourceRepository: "docker.io/library/app"},
67                         },
68                         candidates: []metadata.V2Metadata{
69                                 {Digest: digest.Digest("3"), SourceRepository: "docker.io/user/bar"},
70                                 {Digest: digest.Digest("1"), SourceRepository: "docker.io/user/foo"},
71                                 {Digest: digest.Digest("2"), SourceRepository: "docker.io/library/app"},
72                         },
73                 },
74                 {
75                         name:          "sort more items",
76                         hmacKey:       "abcd",
77                         targetRepo:    "127.0.0.1/foo/bar",
78                         maxCandidates: -1,
79                         metadata: []metadata.V2Metadata{
80                                 taggedMetadata("hash", "1", "docker.io/library/hello-world"),
81                                 taggedMetadata("efgh", "2", "127.0.0.1/hello-world"),
82                                 taggedMetadata("abcd", "3", "docker.io/library/busybox"),
83                                 taggedMetadata("hash", "4", "docker.io/library/busybox"),
84                                 taggedMetadata("hash", "5", "127.0.0.1/foo"),
85                                 taggedMetadata("hash", "6", "127.0.0.1/bar"),
86                                 taggedMetadata("efgh", "7", "127.0.0.1/foo/bar"),
87                                 taggedMetadata("abcd", "8", "127.0.0.1/xyz"),
88                                 taggedMetadata("hash", "9", "127.0.0.1/foo/app"),
89                         },
90                         candidates: []metadata.V2Metadata{
91                                 // first by matching hash
92                                 taggedMetadata("abcd", "8", "127.0.0.1/xyz"),
93                                 // then by longest matching prefix
94                                 taggedMetadata("hash", "9", "127.0.0.1/foo/app"),
95                                 taggedMetadata("hash", "5", "127.0.0.1/foo"),
96                                 // sort the rest of the matching items in reversed order
97                                 taggedMetadata("hash", "6", "127.0.0.1/bar"),
98                                 taggedMetadata("efgh", "2", "127.0.0.1/hello-world"),
99                         },
100                 },
101                 {
102                         name:          "limit max candidates",
103                         hmacKey:       "abcd",
104                         targetRepo:    "user/app",
105                         maxCandidates: 3,
106                         metadata: []metadata.V2Metadata{
107                                 taggedMetadata("abcd", "1", "docker.io/user/app1"),
108                                 taggedMetadata("abcd", "2", "docker.io/user/app/base"),
109                                 taggedMetadata("hash", "3", "docker.io/user/app"),
110                                 taggedMetadata("abcd", "4", "127.0.0.1/user/app"),
111                                 taggedMetadata("hash", "5", "docker.io/user/foo"),
112                                 taggedMetadata("hash", "6", "docker.io/app/bar"),
113                         },
114                         candidates: []metadata.V2Metadata{
115                                 // first by matching hash
116                                 taggedMetadata("abcd", "2", "docker.io/user/app/base"),
117                                 taggedMetadata("abcd", "1", "docker.io/user/app1"),
118                                 // then by longest matching prefix
119                                 // "docker.io/usr/app" is excluded since candidates must
120                                 // be from a different repository
121                                 taggedMetadata("hash", "5", "docker.io/user/foo"),
122                         },
123                 },
124         } {
125                 repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
126                 if err != nil {
127                         t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
128                 }
129                 candidates := getRepositoryMountCandidates(repoInfo, []byte(tc.hmacKey), tc.maxCandidates, tc.metadata)
130                 if len(candidates) != len(tc.candidates) {
131                         t.Errorf("[%s] got unexpected number of candidates: %d != %d", tc.name, len(candidates), len(tc.candidates))
132                 }
133                 for i := 0; i < len(candidates) && i < len(tc.candidates); i++ {
134                         if !reflect.DeepEqual(candidates[i], tc.candidates[i]) {
135                                 t.Errorf("[%s] candidate %d does not match expected: %#+v != %#+v", tc.name, i, candidates[i], tc.candidates[i])
136                         }
137                 }
138                 for i := len(candidates); i < len(tc.candidates); i++ {
139                         t.Errorf("[%s] missing expected candidate at position %d (%#+v)", tc.name, i, tc.candidates[i])
140                 }
141                 for i := len(tc.candidates); i < len(candidates); i++ {
142                         t.Errorf("[%s] got unexpected candidate at position %d (%#+v)", tc.name, i, candidates[i])
143                 }
144         }
145 }
146
147 func TestLayerAlreadyExists(t *testing.T) {
148         for _, tc := range []struct {
149                 name                   string
150                 metadata               []metadata.V2Metadata
151                 targetRepo             string
152                 hmacKey                string
153                 maxExistenceChecks     int
154                 checkOtherRepositories bool
155                 remoteBlobs            map[digest.Digest]distribution.Descriptor
156                 remoteErrors           map[digest.Digest]error
157                 expectedDescriptor     distribution.Descriptor
158                 expectedExists         bool
159                 expectedError          error
160                 expectedRequests       []string
161                 expectedAdditions      []metadata.V2Metadata
162                 expectedRemovals       []metadata.V2Metadata
163         }{
164                 {
165                         name:                   "empty metadata",
166                         targetRepo:             "busybox",
167                         maxExistenceChecks:     3,
168                         checkOtherRepositories: true,
169                 },
170                 {
171                         name:               "single not existent metadata",
172                         targetRepo:         "busybox",
173                         metadata:           []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
174                         maxExistenceChecks: 3,
175                         expectedRequests:   []string{"pear"},
176                         expectedRemovals:   []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
177                 },
178                 {
179                         name:               "access denied",
180                         targetRepo:         "busybox",
181                         maxExistenceChecks: 1,
182                         metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
183                         remoteErrors:       map[digest.Digest]error{digest.Digest("apple"): distribution.ErrAccessDenied},
184                         expectedError:      nil,
185                         expectedRequests:   []string{"apple"},
186                 },
187                 {
188                         name:               "not matching reposies",
189                         targetRepo:         "busybox",
190                         maxExistenceChecks: 3,
191                         metadata: []metadata.V2Metadata{
192                                 {Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/hello-world"},
193                                 {Digest: digest.Digest("orange"), SourceRepository: "docker.io/library/busybox/subapp"},
194                                 {Digest: digest.Digest("pear"), SourceRepository: "docker.io/busybox"},
195                                 {Digest: digest.Digest("plum"), SourceRepository: "busybox"},
196                                 {Digest: digest.Digest("banana"), SourceRepository: "127.0.0.1/busybox"},
197                         },
198                 },
199                 {
200                         name:                   "check other repositories",
201                         targetRepo:             "busybox",
202                         maxExistenceChecks:     10,
203                         checkOtherRepositories: true,
204                         metadata: []metadata.V2Metadata{
205                                 {Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/hello-world"},
206                                 {Digest: digest.Digest("orange"), SourceRepository: "docker.io/busybox/subapp"},
207                                 {Digest: digest.Digest("pear"), SourceRepository: "docker.io/busybox"},
208                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
209                                 {Digest: digest.Digest("banana"), SourceRepository: "127.0.0.1/busybox"},
210                         },
211                         expectedRequests: []string{"plum", "apple", "pear", "orange", "banana"},
212                         expectedRemovals: []metadata.V2Metadata{
213                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
214                         },
215                 },
216                 {
217                         name:               "find existing blob",
218                         targetRepo:         "busybox",
219                         metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
220                         maxExistenceChecks: 3,
221                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple")}},
222                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
223                         expectedExists:     true,
224                         expectedRequests:   []string{"apple"},
225                 },
226                 {
227                         name:               "find existing blob with different hmac",
228                         targetRepo:         "busybox",
229                         metadata:           []metadata.V2Metadata{{SourceRepository: "docker.io/library/busybox", Digest: digest.Digest("apple"), HMAC: "dummyhmac"}},
230                         maxExistenceChecks: 3,
231                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple")}},
232                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
233                         expectedExists:     true,
234                         expectedRequests:   []string{"apple"},
235                         expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
236                 },
237                 {
238                         name:               "overwrite media types",
239                         targetRepo:         "busybox",
240                         metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
241                         hmacKey:            "key",
242                         maxExistenceChecks: 3,
243                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple"), MediaType: "custom-media-type"}},
244                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
245                         expectedExists:     true,
246                         expectedRequests:   []string{"apple"},
247                         expectedAdditions:  []metadata.V2Metadata{taggedMetadata("key", "apple", "docker.io/library/busybox")},
248                 },
249                 {
250                         name:       "find existing blob among many",
251                         targetRepo: "127.0.0.1/myapp",
252                         hmacKey:    "key",
253                         metadata: []metadata.V2Metadata{
254                                 taggedMetadata("someotherkey", "pear", "127.0.0.1/myapp"),
255                                 taggedMetadata("key", "apple", "127.0.0.1/myapp"),
256                                 taggedMetadata("", "plum", "127.0.0.1/myapp"),
257                         },
258                         maxExistenceChecks: 3,
259                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
260                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("pear"), MediaType: schema2.MediaTypeLayer},
261                         expectedExists:     true,
262                         expectedRequests:   []string{"apple", "plum", "pear"},
263                         expectedAdditions:  []metadata.V2Metadata{taggedMetadata("key", "pear", "127.0.0.1/myapp")},
264                         expectedRemovals: []metadata.V2Metadata{
265                                 taggedMetadata("key", "apple", "127.0.0.1/myapp"),
266                                 {Digest: digest.Digest("plum"), SourceRepository: "127.0.0.1/myapp"},
267                         },
268                 },
269                 {
270                         name:       "reach maximum existence checks",
271                         targetRepo: "user/app",
272                         metadata: []metadata.V2Metadata{
273                                 {Digest: digest.Digest("pear"), SourceRepository: "docker.io/user/app"},
274                                 {Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
275                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
276                                 {Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
277                         },
278                         maxExistenceChecks: 3,
279                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
280                         expectedExists:     false,
281                         expectedRequests:   []string{"banana", "plum", "apple"},
282                         expectedRemovals: []metadata.V2Metadata{
283                                 {Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
284                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
285                                 {Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
286                         },
287                 },
288                 {
289                         name:       "zero allowed existence checks",
290                         targetRepo: "user/app",
291                         metadata: []metadata.V2Metadata{
292                                 {Digest: digest.Digest("pear"), SourceRepository: "docker.io/user/app"},
293                                 {Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
294                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
295                                 {Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
296                         },
297                         maxExistenceChecks: 0,
298                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
299                 },
300                 {
301                         name:       "stat single digest just once",
302                         targetRepo: "busybox",
303                         metadata: []metadata.V2Metadata{
304                                 taggedMetadata("key1", "pear", "docker.io/library/busybox"),
305                                 taggedMetadata("key2", "apple", "docker.io/library/busybox"),
306                                 taggedMetadata("key3", "apple", "docker.io/library/busybox"),
307                         },
308                         maxExistenceChecks: 3,
309                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
310                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("pear"), MediaType: schema2.MediaTypeLayer},
311                         expectedExists:     true,
312                         expectedRequests:   []string{"apple", "pear"},
313                         expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
314                         expectedRemovals:   []metadata.V2Metadata{taggedMetadata("key3", "apple", "docker.io/library/busybox")},
315                 },
316                 {
317                         name:       "don't stop on first error",
318                         targetRepo: "user/app",
319                         hmacKey:    "key",
320                         metadata: []metadata.V2Metadata{
321                                 taggedMetadata("key", "banana", "docker.io/user/app"),
322                                 taggedMetadata("key", "orange", "docker.io/user/app"),
323                                 taggedMetadata("key", "plum", "docker.io/user/app"),
324                         },
325                         maxExistenceChecks: 3,
326                         remoteErrors:       map[digest.Digest]error{"orange": distribution.ErrAccessDenied},
327                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {}},
328                         expectedError:      nil,
329                         expectedRequests:   []string{"plum", "orange", "banana"},
330                         expectedRemovals: []metadata.V2Metadata{
331                                 taggedMetadata("key", "plum", "docker.io/user/app"),
332                                 taggedMetadata("key", "banana", "docker.io/user/app"),
333                         },
334                 },
335                 {
336                         name:       "remove outdated metadata",
337                         targetRepo: "docker.io/user/app",
338                         metadata: []metadata.V2Metadata{
339                                 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
340                                 {Digest: digest.Digest("orange"), SourceRepository: "docker.io/user/app"},
341                         },
342                         maxExistenceChecks: 3,
343                         remoteErrors:       map[digest.Digest]error{"orange": distribution.ErrBlobUnknown},
344                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("plum"): {}},
345                         expectedExists:     false,
346                         expectedRequests:   []string{"orange"},
347                         expectedRemovals:   []metadata.V2Metadata{{Digest: digest.Digest("orange"), SourceRepository: "docker.io/user/app"}},
348                 },
349                 {
350                         name:       "missing SourceRepository",
351                         targetRepo: "busybox",
352                         metadata: []metadata.V2Metadata{
353                                 {Digest: digest.Digest("1")},
354                                 {Digest: digest.Digest("3")},
355                                 {Digest: digest.Digest("2")},
356                         },
357                         maxExistenceChecks: 3,
358                         expectedExists:     false,
359                         expectedRequests:   []string{"2", "3", "1"},
360                 },
361
362                 {
363                         name:       "with and without SourceRepository",
364                         targetRepo: "busybox",
365                         metadata: []metadata.V2Metadata{
366                                 {Digest: digest.Digest("1")},
367                                 {Digest: digest.Digest("2"), SourceRepository: "docker.io/library/busybox"},
368                                 {Digest: digest.Digest("3")},
369                         },
370                         remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("1"): {Digest: digest.Digest("1")}},
371                         maxExistenceChecks: 3,
372                         expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("1"), MediaType: schema2.MediaTypeLayer},
373                         expectedExists:     true,
374                         expectedRequests:   []string{"2", "3", "1"},
375                         expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("1"), SourceRepository: "docker.io/library/busybox"}},
376                         expectedRemovals: []metadata.V2Metadata{
377                                 {Digest: digest.Digest("2"), SourceRepository: "docker.io/library/busybox"},
378                         },
379                 },
380         } {
381                 repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
382                 if err != nil {
383                         t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
384                 }
385                 repo := &mockRepo{
386                         t:        t,
387                         errors:   tc.remoteErrors,
388                         blobs:    tc.remoteBlobs,
389                         requests: []string{},
390                 }
391                 ctx := context.Background()
392                 ms := &mockV2MetadataService{}
393                 pd := &v2PushDescriptor{
394                         hmacKey:  []byte(tc.hmacKey),
395                         repoInfo: repoInfo,
396                         layer: &storeLayer{
397                                 Layer: layer.EmptyLayer,
398                         },
399                         repo:              repo,
400                         v2MetadataService: ms,
401                         pushState:         &pushState{remoteLayers: make(map[layer.DiffID]distribution.Descriptor)},
402                         checkedDigests:    make(map[digest.Digest]struct{}),
403                 }
404
405                 desc, exists, err := pd.layerAlreadyExists(ctx, &progressSink{t}, layer.EmptyLayer.DiffID(), tc.checkOtherRepositories, tc.maxExistenceChecks, tc.metadata)
406
407                 if !reflect.DeepEqual(desc, tc.expectedDescriptor) {
408                         t.Errorf("[%s] got unexpected descriptor: %#+v != %#+v", tc.name, desc, tc.expectedDescriptor)
409                 }
410                 if exists != tc.expectedExists {
411                         t.Errorf("[%s] got unexpected exists: %t != %t", tc.name, exists, tc.expectedExists)
412                 }
413                 if !reflect.DeepEqual(err, tc.expectedError) {
414                         t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
415                 }
416
417                 if len(repo.requests) != len(tc.expectedRequests) {
418                         t.Errorf("[%s] got unexpected number of requests: %d != %d", tc.name, len(repo.requests), len(tc.expectedRequests))
419                 }
420                 for i := 0; i < len(repo.requests) && i < len(tc.expectedRequests); i++ {
421                         if repo.requests[i] != tc.expectedRequests[i] {
422                                 t.Errorf("[%s] request %d does not match expected: %q != %q", tc.name, i, repo.requests[i], tc.expectedRequests[i])
423                         }
424                 }
425                 for i := len(repo.requests); i < len(tc.expectedRequests); i++ {
426                         t.Errorf("[%s] missing expected request at position %d (%q)", tc.name, i, tc.expectedRequests[i])
427                 }
428                 for i := len(tc.expectedRequests); i < len(repo.requests); i++ {
429                         t.Errorf("[%s] got unexpected request at position %d (%q)", tc.name, i, repo.requests[i])
430                 }
431
432                 if len(ms.added) != len(tc.expectedAdditions) {
433                         t.Errorf("[%s] got unexpected number of additions: %d != %d", tc.name, len(ms.added), len(tc.expectedAdditions))
434                 }
435                 for i := 0; i < len(ms.added) && i < len(tc.expectedAdditions); i++ {
436                         if ms.added[i] != tc.expectedAdditions[i] {
437                                 t.Errorf("[%s] added metadata at %d does not match expected: %q != %q", tc.name, i, ms.added[i], tc.expectedAdditions[i])
438                         }
439                 }
440                 for i := len(ms.added); i < len(tc.expectedAdditions); i++ {
441                         t.Errorf("[%s] missing expected addition at position %d (%q)", tc.name, i, tc.expectedAdditions[i])
442                 }
443                 for i := len(tc.expectedAdditions); i < len(ms.added); i++ {
444                         t.Errorf("[%s] unexpected metadata addition at position %d (%q)", tc.name, i, ms.added[i])
445                 }
446
447                 if len(ms.removed) != len(tc.expectedRemovals) {
448                         t.Errorf("[%s] got unexpected number of removals: %d != %d", tc.name, len(ms.removed), len(tc.expectedRemovals))
449                 }
450                 for i := 0; i < len(ms.removed) && i < len(tc.expectedRemovals); i++ {
451                         if ms.removed[i] != tc.expectedRemovals[i] {
452                                 t.Errorf("[%s] removed metadata at %d does not match expected: %q != %q", tc.name, i, ms.removed[i], tc.expectedRemovals[i])
453                         }
454                 }
455                 for i := len(ms.removed); i < len(tc.expectedRemovals); i++ {
456                         t.Errorf("[%s] missing expected removal at position %d (%q)", tc.name, i, tc.expectedRemovals[i])
457                 }
458                 for i := len(tc.expectedRemovals); i < len(ms.removed); i++ {
459                         t.Errorf("[%s] removed unexpected metadata at position %d (%q)", tc.name, i, ms.removed[i])
460                 }
461         }
462 }
463
464 func taggedMetadata(key string, dgst string, sourceRepo string) metadata.V2Metadata {
465         meta := metadata.V2Metadata{
466                 Digest:           digest.Digest(dgst),
467                 SourceRepository: sourceRepo,
468         }
469
470         meta.HMAC = metadata.ComputeV2MetadataHMAC([]byte(key), &meta)
471         return meta
472 }
473
474 type mockRepo struct {
475         t        *testing.T
476         errors   map[digest.Digest]error
477         blobs    map[digest.Digest]distribution.Descriptor
478         requests []string
479 }
480
481 var _ distribution.Repository = &mockRepo{}
482
483 func (m *mockRepo) Named() reference.Named {
484         m.t.Fatalf("Named() not implemented")
485         return nil
486 }
487 func (m *mockRepo) Manifests(ctc context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
488         m.t.Fatalf("Manifests() not implemented")
489         return nil, nil
490 }
491 func (m *mockRepo) Tags(ctc context.Context) distribution.TagService {
492         m.t.Fatalf("Tags() not implemented")
493         return nil
494 }
495 func (m *mockRepo) Blobs(ctx context.Context) distribution.BlobStore {
496         return &mockBlobStore{
497                 repo: m,
498         }
499 }
500
501 type mockBlobStore struct {
502         repo *mockRepo
503 }
504
505 var _ distribution.BlobStore = &mockBlobStore{}
506
507 func (m *mockBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
508         m.repo.requests = append(m.repo.requests, dgst.String())
509         if err, exists := m.repo.errors[dgst]; exists {
510                 return distribution.Descriptor{}, err
511         }
512         if desc, exists := m.repo.blobs[dgst]; exists {
513                 return desc, nil
514         }
515         return distribution.Descriptor{}, distribution.ErrBlobUnknown
516 }
517 func (m *mockBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
518         m.repo.t.Fatal("Get() not implemented")
519         return nil, nil
520 }
521
522 func (m *mockBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
523         m.repo.t.Fatal("Open() not implemented")
524         return nil, nil
525 }
526
527 func (m *mockBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
528         m.repo.t.Fatal("Put() not implemented")
529         return distribution.Descriptor{}, nil
530 }
531
532 func (m *mockBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
533         m.repo.t.Fatal("Create() not implemented")
534         return nil, nil
535 }
536 func (m *mockBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
537         m.repo.t.Fatal("Resume() not implemented")
538         return nil, nil
539 }
540 func (m *mockBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
541         m.repo.t.Fatal("Delete() not implemented")
542         return nil
543 }
544 func (m *mockBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
545         m.repo.t.Fatalf("ServeBlob() not implemented")
546         return nil
547 }
548
549 type mockV2MetadataService struct {
550         added   []metadata.V2Metadata
551         removed []metadata.V2Metadata
552 }
553
554 var _ metadata.V2MetadataService = &mockV2MetadataService{}
555
556 func (*mockV2MetadataService) GetMetadata(diffID layer.DiffID) ([]metadata.V2Metadata, error) {
557         return nil, nil
558 }
559 func (*mockV2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) {
560         return "", nil
561 }
562 func (m *mockV2MetadataService) Add(diffID layer.DiffID, metadata metadata.V2Metadata) error {
563         m.added = append(m.added, metadata)
564         return nil
565 }
566 func (m *mockV2MetadataService) TagAndAdd(diffID layer.DiffID, hmacKey []byte, meta metadata.V2Metadata) error {
567         meta.HMAC = metadata.ComputeV2MetadataHMAC(hmacKey, &meta)
568         m.Add(diffID, meta)
569         return nil
570 }
571 func (m *mockV2MetadataService) Remove(metadata metadata.V2Metadata) error {
572         m.removed = append(m.removed, metadata)
573         return nil
574 }
575
576 type progressSink struct {
577         t *testing.T
578 }
579
580 func (s *progressSink) WriteProgress(p progress.Progress) error {
581         s.t.Logf("progress update: %#+v", p)
582         return nil
583 }