github.com/skf/moby@v1.13.1/distribution/push_v2_test.go (about)

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