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"
18 func TestGetRepositoryMountCandidates(t *testing.T) {
19 for _, tc := range []struct {
24 metadata []metadata.V2Metadata
25 candidates []metadata.V2Metadata
28 name: "empty metadata",
29 targetRepo: "busybox",
31 metadata: []metadata.V2Metadata{},
32 candidates: []metadata.V2Metadata{},
35 name: "one item not matching",
36 targetRepo: "busybox",
38 metadata: []metadata.V2Metadata{taggedMetadata("key", "dgst", "127.0.0.1/repo")},
39 candidates: []metadata.V2Metadata{},
42 name: "one item matching",
43 targetRepo: "busybox",
45 metadata: []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
46 candidates: []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
49 name: "allow missing SourceRepository",
50 targetRepo: "busybox",
52 metadata: []metadata.V2Metadata{
53 {Digest: digest.Digest("1")},
54 {Digest: digest.Digest("3")},
55 {Digest: digest.Digest("2")},
57 candidates: []metadata.V2Metadata{},
60 name: "handle docker.io",
61 targetRepo: "user/app",
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"},
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"},
75 name: "sort more items",
77 targetRepo: "127.0.0.1/foo/bar",
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"),
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"),
102 name: "limit max candidates",
104 targetRepo: "user/app",
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"),
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"),
125 repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
127 t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
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))
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])
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])
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])
147 func TestLayerAlreadyExists(t *testing.T) {
148 for _, tc := range []struct {
150 metadata []metadata.V2Metadata
153 maxExistenceChecks int
154 checkOtherRepositories bool
155 remoteBlobs map[digest.Digest]distribution.Descriptor
156 remoteErrors map[digest.Digest]error
157 expectedDescriptor distribution.Descriptor
160 expectedRequests []string
161 expectedAdditions []metadata.V2Metadata
162 expectedRemovals []metadata.V2Metadata
165 name: "empty metadata",
166 targetRepo: "busybox",
167 maxExistenceChecks: 3,
168 checkOtherRepositories: true,
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"}},
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},
185 expectedRequests: []string{"apple"},
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"},
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"},
211 expectedRequests: []string{"plum", "apple", "pear", "orange", "banana"},
212 expectedRemovals: []metadata.V2Metadata{
213 {Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
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"},
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"}},
238 name: "overwrite media types",
239 targetRepo: "busybox",
240 metadata: []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
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")},
250 name: "find existing blob among many",
251 targetRepo: "127.0.0.1/myapp",
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"),
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"},
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"},
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"},
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"},
297 maxExistenceChecks: 0,
298 remoteBlobs: map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
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"),
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")},
317 name: "don't stop on first error",
318 targetRepo: "user/app",
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"),
325 maxExistenceChecks: 3,
326 remoteErrors: map[digest.Digest]error{"orange": distribution.ErrAccessDenied},
327 remoteBlobs: map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {}},
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"),
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"},
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"}},
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")},
357 maxExistenceChecks: 3,
358 expectedExists: false,
359 expectedRequests: []string{"2", "3", "1"},
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")},
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"},
381 repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
383 t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
387 errors: tc.remoteErrors,
388 blobs: tc.remoteBlobs,
389 requests: []string{},
391 ctx := context.Background()
392 ms := &mockV2MetadataService{}
393 pd := &v2PushDescriptor{
394 hmacKey: []byte(tc.hmacKey),
397 Layer: layer.EmptyLayer,
400 v2MetadataService: ms,
401 pushState: &pushState{remoteLayers: make(map[layer.DiffID]distribution.Descriptor)},
402 checkedDigests: make(map[digest.Digest]struct{}),
405 desc, exists, err := pd.layerAlreadyExists(ctx, &progressSink{t}, layer.EmptyLayer.DiffID(), tc.checkOtherRepositories, tc.maxExistenceChecks, tc.metadata)
407 if !reflect.DeepEqual(desc, tc.expectedDescriptor) {
408 t.Errorf("[%s] got unexpected descriptor: %#+v != %#+v", tc.name, desc, tc.expectedDescriptor)
410 if exists != tc.expectedExists {
411 t.Errorf("[%s] got unexpected exists: %t != %t", tc.name, exists, tc.expectedExists)
413 if !reflect.DeepEqual(err, tc.expectedError) {
414 t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
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))
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])
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])
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])
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))
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])
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])
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])
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))
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])
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])
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])
464 func taggedMetadata(key string, dgst string, sourceRepo string) metadata.V2Metadata {
465 meta := metadata.V2Metadata{
466 Digest: digest.Digest(dgst),
467 SourceRepository: sourceRepo,
470 meta.HMAC = metadata.ComputeV2MetadataHMAC([]byte(key), &meta)
474 type mockRepo struct {
476 errors map[digest.Digest]error
477 blobs map[digest.Digest]distribution.Descriptor
481 var _ distribution.Repository = &mockRepo{}
483 func (m *mockRepo) Named() reference.Named {
484 m.t.Fatalf("Named() not implemented")
487 func (m *mockRepo) Manifests(ctc context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
488 m.t.Fatalf("Manifests() not implemented")
491 func (m *mockRepo) Tags(ctc context.Context) distribution.TagService {
492 m.t.Fatalf("Tags() not implemented")
495 func (m *mockRepo) Blobs(ctx context.Context) distribution.BlobStore {
496 return &mockBlobStore{
501 type mockBlobStore struct {
505 var _ distribution.BlobStore = &mockBlobStore{}
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
512 if desc, exists := m.repo.blobs[dgst]; exists {
515 return distribution.Descriptor{}, distribution.ErrBlobUnknown
517 func (m *mockBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
518 m.repo.t.Fatal("Get() not implemented")
522 func (m *mockBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
523 m.repo.t.Fatal("Open() not implemented")
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
532 func (m *mockBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
533 m.repo.t.Fatal("Create() not implemented")
536 func (m *mockBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
537 m.repo.t.Fatal("Resume() not implemented")
540 func (m *mockBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
541 m.repo.t.Fatal("Delete() not implemented")
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")
549 type mockV2MetadataService struct {
550 added []metadata.V2Metadata
551 removed []metadata.V2Metadata
554 var _ metadata.V2MetadataService = &mockV2MetadataService{}
556 func (*mockV2MetadataService) GetMetadata(diffID layer.DiffID) ([]metadata.V2Metadata, error) {
559 func (*mockV2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) {
562 func (m *mockV2MetadataService) Add(diffID layer.DiffID, metadata metadata.V2Metadata) error {
563 m.added = append(m.added, metadata)
566 func (m *mockV2MetadataService) TagAndAdd(diffID layer.DiffID, hmacKey []byte, meta metadata.V2Metadata) error {
567 meta.HMAC = metadata.ComputeV2MetadataHMAC(hmacKey, &meta)
571 func (m *mockV2MetadataService) Remove(metadata metadata.V2Metadata) error {
572 m.removed = append(m.removed, metadata)
576 type progressSink struct {
580 func (s *progressSink) WriteProgress(p progress.Progress) error {
581 s.t.Logf("progress update: %#+v", p)