github.com/lazyboychen7/engine@v17.12.1-ce-rc2+incompatible/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/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 repositories",
   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  }