github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/distribution/push_v2_test.go (about)

     1  package distribution // import "github.com/docker/docker/distribution"
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/url"
     7  	"reflect"
     8  	"testing"
     9  
    10  	"github.com/docker/distribution"
    11  	"github.com/docker/distribution/manifest/schema2"
    12  	"github.com/docker/distribution/reference"
    13  	"github.com/docker/distribution/registry/api/errcode"
    14  	"github.com/docker/docker/api/types"
    15  	"github.com/docker/docker/distribution/metadata"
    16  	"github.com/docker/docker/layer"
    17  	"github.com/docker/docker/pkg/progress"
    18  	refstore "github.com/docker/docker/reference"
    19  	"github.com/docker/docker/registry"
    20  	digest "github.com/opencontainers/go-digest"
    21  )
    22  
    23  func TestGetRepositoryMountCandidates(t *testing.T) {
    24  	for _, tc := range []struct {
    25  		name          string
    26  		hmacKey       string
    27  		targetRepo    string
    28  		maxCandidates int
    29  		metadata      []metadata.V2Metadata
    30  		candidates    []metadata.V2Metadata
    31  	}{
    32  		{
    33  			name:          "empty metadata",
    34  			targetRepo:    "busybox",
    35  			maxCandidates: -1,
    36  			metadata:      []metadata.V2Metadata{},
    37  			candidates:    []metadata.V2Metadata{},
    38  		},
    39  		{
    40  			name:          "one item not matching",
    41  			targetRepo:    "busybox",
    42  			maxCandidates: -1,
    43  			metadata:      []metadata.V2Metadata{taggedMetadata("key", "dgst", "127.0.0.1/repo")},
    44  			candidates:    []metadata.V2Metadata{},
    45  		},
    46  		{
    47  			name:          "one item matching",
    48  			targetRepo:    "busybox",
    49  			maxCandidates: -1,
    50  			metadata:      []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
    51  			candidates:    []metadata.V2Metadata{taggedMetadata("hash", "1", "docker.io/library/hello-world")},
    52  		},
    53  		{
    54  			name:          "allow missing SourceRepository",
    55  			targetRepo:    "busybox",
    56  			maxCandidates: -1,
    57  			metadata: []metadata.V2Metadata{
    58  				{Digest: digest.Digest("1")},
    59  				{Digest: digest.Digest("3")},
    60  				{Digest: digest.Digest("2")},
    61  			},
    62  			candidates: []metadata.V2Metadata{},
    63  		},
    64  		{
    65  			name:          "handle docker.io",
    66  			targetRepo:    "user/app",
    67  			maxCandidates: -1,
    68  			metadata: []metadata.V2Metadata{
    69  				{Digest: digest.Digest("1"), SourceRepository: "docker.io/user/foo"},
    70  				{Digest: digest.Digest("3"), SourceRepository: "docker.io/user/bar"},
    71  				{Digest: digest.Digest("2"), SourceRepository: "docker.io/library/app"},
    72  			},
    73  			candidates: []metadata.V2Metadata{
    74  				{Digest: digest.Digest("3"), SourceRepository: "docker.io/user/bar"},
    75  				{Digest: digest.Digest("1"), SourceRepository: "docker.io/user/foo"},
    76  				{Digest: digest.Digest("2"), SourceRepository: "docker.io/library/app"},
    77  			},
    78  		},
    79  		{
    80  			name:          "sort more items",
    81  			hmacKey:       "abcd",
    82  			targetRepo:    "127.0.0.1/foo/bar",
    83  			maxCandidates: -1,
    84  			metadata: []metadata.V2Metadata{
    85  				taggedMetadata("hash", "1", "docker.io/library/hello-world"),
    86  				taggedMetadata("efgh", "2", "127.0.0.1/hello-world"),
    87  				taggedMetadata("abcd", "3", "docker.io/library/busybox"),
    88  				taggedMetadata("hash", "4", "docker.io/library/busybox"),
    89  				taggedMetadata("hash", "5", "127.0.0.1/foo"),
    90  				taggedMetadata("hash", "6", "127.0.0.1/bar"),
    91  				taggedMetadata("efgh", "7", "127.0.0.1/foo/bar"),
    92  				taggedMetadata("abcd", "8", "127.0.0.1/xyz"),
    93  				taggedMetadata("hash", "9", "127.0.0.1/foo/app"),
    94  			},
    95  			candidates: []metadata.V2Metadata{
    96  				// first by matching hash
    97  				taggedMetadata("abcd", "8", "127.0.0.1/xyz"),
    98  				// then by longest matching prefix
    99  				taggedMetadata("hash", "9", "127.0.0.1/foo/app"),
   100  				taggedMetadata("hash", "5", "127.0.0.1/foo"),
   101  				// sort the rest of the matching items in reversed order
   102  				taggedMetadata("hash", "6", "127.0.0.1/bar"),
   103  				taggedMetadata("efgh", "2", "127.0.0.1/hello-world"),
   104  			},
   105  		},
   106  		{
   107  			name:          "limit max candidates",
   108  			hmacKey:       "abcd",
   109  			targetRepo:    "user/app",
   110  			maxCandidates: 3,
   111  			metadata: []metadata.V2Metadata{
   112  				taggedMetadata("abcd", "1", "docker.io/user/app1"),
   113  				taggedMetadata("abcd", "2", "docker.io/user/app/base"),
   114  				taggedMetadata("hash", "3", "docker.io/user/app"),
   115  				taggedMetadata("abcd", "4", "127.0.0.1/user/app"),
   116  				taggedMetadata("hash", "5", "docker.io/user/foo"),
   117  				taggedMetadata("hash", "6", "docker.io/app/bar"),
   118  			},
   119  			candidates: []metadata.V2Metadata{
   120  				// first by matching hash
   121  				taggedMetadata("abcd", "2", "docker.io/user/app/base"),
   122  				taggedMetadata("abcd", "1", "docker.io/user/app1"),
   123  				// then by longest matching prefix
   124  				// "docker.io/usr/app" is excluded since candidates must
   125  				// be from a different repository
   126  				taggedMetadata("hash", "5", "docker.io/user/foo"),
   127  			},
   128  		},
   129  	} {
   130  		repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
   131  		if err != nil {
   132  			t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
   133  		}
   134  		candidates := getRepositoryMountCandidates(repoInfo, []byte(tc.hmacKey), tc.maxCandidates, tc.metadata)
   135  		if len(candidates) != len(tc.candidates) {
   136  			t.Errorf("[%s] got unexpected number of candidates: %d != %d", tc.name, len(candidates), len(tc.candidates))
   137  		}
   138  		for i := 0; i < len(candidates) && i < len(tc.candidates); i++ {
   139  			if !reflect.DeepEqual(candidates[i], tc.candidates[i]) {
   140  				t.Errorf("[%s] candidate %d does not match expected: %#+v != %#+v", tc.name, i, candidates[i], tc.candidates[i])
   141  			}
   142  		}
   143  		for i := len(candidates); i < len(tc.candidates); i++ {
   144  			t.Errorf("[%s] missing expected candidate at position %d (%#+v)", tc.name, i, tc.candidates[i])
   145  		}
   146  		for i := len(tc.candidates); i < len(candidates); i++ {
   147  			t.Errorf("[%s] got unexpected candidate at position %d (%#+v)", tc.name, i, candidates[i])
   148  		}
   149  	}
   150  }
   151  
   152  func TestLayerAlreadyExists(t *testing.T) {
   153  	for _, tc := range []struct {
   154  		name                   string
   155  		metadata               []metadata.V2Metadata
   156  		targetRepo             string
   157  		hmacKey                string
   158  		maxExistenceChecks     int
   159  		checkOtherRepositories bool
   160  		remoteBlobs            map[digest.Digest]distribution.Descriptor
   161  		remoteErrors           map[digest.Digest]error
   162  		expectedDescriptor     distribution.Descriptor
   163  		expectedExists         bool
   164  		expectedError          error
   165  		expectedRequests       []string
   166  		expectedAdditions      []metadata.V2Metadata
   167  		expectedRemovals       []metadata.V2Metadata
   168  	}{
   169  		{
   170  			name:                   "empty metadata",
   171  			targetRepo:             "busybox",
   172  			maxExistenceChecks:     3,
   173  			checkOtherRepositories: true,
   174  		},
   175  		{
   176  			name:               "single not existent metadata",
   177  			targetRepo:         "busybox",
   178  			metadata:           []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
   179  			maxExistenceChecks: 3,
   180  			expectedRequests:   []string{"pear"},
   181  			expectedRemovals:   []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
   182  		},
   183  		{
   184  			name:               "access denied",
   185  			targetRepo:         "busybox",
   186  			maxExistenceChecks: 1,
   187  			metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
   188  			remoteErrors:       map[digest.Digest]error{digest.Digest("apple"): distribution.ErrAccessDenied},
   189  			expectedError:      nil,
   190  			expectedRequests:   []string{"apple"},
   191  		},
   192  		{
   193  			name:               "not matching repositories",
   194  			targetRepo:         "busybox",
   195  			maxExistenceChecks: 3,
   196  			metadata: []metadata.V2Metadata{
   197  				{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/hello-world"},
   198  				{Digest: digest.Digest("orange"), SourceRepository: "docker.io/library/busybox/subapp"},
   199  				{Digest: digest.Digest("pear"), SourceRepository: "docker.io/busybox"},
   200  				{Digest: digest.Digest("plum"), SourceRepository: "busybox"},
   201  				{Digest: digest.Digest("banana"), SourceRepository: "127.0.0.1/busybox"},
   202  			},
   203  		},
   204  		{
   205  			name:                   "check other repositories",
   206  			targetRepo:             "busybox",
   207  			maxExistenceChecks:     10,
   208  			checkOtherRepositories: true,
   209  			metadata: []metadata.V2Metadata{
   210  				{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/hello-world"},
   211  				{Digest: digest.Digest("orange"), SourceRepository: "docker.io/busybox/subapp"},
   212  				{Digest: digest.Digest("pear"), SourceRepository: "docker.io/busybox"},
   213  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
   214  				{Digest: digest.Digest("banana"), SourceRepository: "127.0.0.1/busybox"},
   215  			},
   216  			expectedRequests: []string{"plum", "apple", "pear", "orange", "banana"},
   217  			expectedRemovals: []metadata.V2Metadata{
   218  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
   219  			},
   220  		},
   221  		{
   222  			name:               "find existing blob",
   223  			targetRepo:         "busybox",
   224  			metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
   225  			maxExistenceChecks: 3,
   226  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple")}},
   227  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
   228  			expectedExists:     true,
   229  			expectedRequests:   []string{"apple"},
   230  		},
   231  		{
   232  			name:               "find existing blob with different hmac",
   233  			targetRepo:         "busybox",
   234  			metadata:           []metadata.V2Metadata{{SourceRepository: "docker.io/library/busybox", Digest: digest.Digest("apple"), HMAC: "dummyhmac"}},
   235  			maxExistenceChecks: 3,
   236  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple")}},
   237  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
   238  			expectedExists:     true,
   239  			expectedRequests:   []string{"apple"},
   240  			expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
   241  		},
   242  		{
   243  			name:               "overwrite media types",
   244  			targetRepo:         "busybox",
   245  			metadata:           []metadata.V2Metadata{{Digest: digest.Digest("apple"), SourceRepository: "docker.io/library/busybox"}},
   246  			hmacKey:            "key",
   247  			maxExistenceChecks: 3,
   248  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {Digest: digest.Digest("apple"), MediaType: "custom-media-type"}},
   249  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("apple"), MediaType: schema2.MediaTypeLayer},
   250  			expectedExists:     true,
   251  			expectedRequests:   []string{"apple"},
   252  			expectedAdditions:  []metadata.V2Metadata{taggedMetadata("key", "apple", "docker.io/library/busybox")},
   253  		},
   254  		{
   255  			name:       "find existing blob among many",
   256  			targetRepo: "127.0.0.1/myapp",
   257  			hmacKey:    "key",
   258  			metadata: []metadata.V2Metadata{
   259  				taggedMetadata("someotherkey", "pear", "127.0.0.1/myapp"),
   260  				taggedMetadata("key", "apple", "127.0.0.1/myapp"),
   261  				taggedMetadata("", "plum", "127.0.0.1/myapp"),
   262  			},
   263  			maxExistenceChecks: 3,
   264  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
   265  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("pear"), MediaType: schema2.MediaTypeLayer},
   266  			expectedExists:     true,
   267  			expectedRequests:   []string{"apple", "plum", "pear"},
   268  			expectedAdditions:  []metadata.V2Metadata{taggedMetadata("key", "pear", "127.0.0.1/myapp")},
   269  			expectedRemovals: []metadata.V2Metadata{
   270  				taggedMetadata("key", "apple", "127.0.0.1/myapp"),
   271  				{Digest: digest.Digest("plum"), SourceRepository: "127.0.0.1/myapp"},
   272  			},
   273  		},
   274  		{
   275  			name:       "reach maximum existence checks",
   276  			targetRepo: "user/app",
   277  			metadata: []metadata.V2Metadata{
   278  				{Digest: digest.Digest("pear"), SourceRepository: "docker.io/user/app"},
   279  				{Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
   280  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
   281  				{Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
   282  			},
   283  			maxExistenceChecks: 3,
   284  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
   285  			expectedExists:     false,
   286  			expectedRequests:   []string{"banana", "plum", "apple"},
   287  			expectedRemovals: []metadata.V2Metadata{
   288  				{Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
   289  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
   290  				{Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
   291  			},
   292  		},
   293  		{
   294  			name:       "zero allowed existence checks",
   295  			targetRepo: "user/app",
   296  			metadata: []metadata.V2Metadata{
   297  				{Digest: digest.Digest("pear"), SourceRepository: "docker.io/user/app"},
   298  				{Digest: digest.Digest("apple"), SourceRepository: "docker.io/user/app"},
   299  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/user/app"},
   300  				{Digest: digest.Digest("banana"), SourceRepository: "docker.io/user/app"},
   301  			},
   302  			maxExistenceChecks: 0,
   303  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
   304  		},
   305  		{
   306  			name:       "stat single digest just once",
   307  			targetRepo: "busybox",
   308  			metadata: []metadata.V2Metadata{
   309  				taggedMetadata("key1", "pear", "docker.io/library/busybox"),
   310  				taggedMetadata("key2", "apple", "docker.io/library/busybox"),
   311  				taggedMetadata("key3", "apple", "docker.io/library/busybox"),
   312  			},
   313  			maxExistenceChecks: 3,
   314  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("pear"): {Digest: digest.Digest("pear")}},
   315  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("pear"), MediaType: schema2.MediaTypeLayer},
   316  			expectedExists:     true,
   317  			expectedRequests:   []string{"apple", "pear"},
   318  			expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("pear"), SourceRepository: "docker.io/library/busybox"}},
   319  			expectedRemovals:   []metadata.V2Metadata{taggedMetadata("key3", "apple", "docker.io/library/busybox")},
   320  		},
   321  		{
   322  			name:       "don't stop on first error",
   323  			targetRepo: "user/app",
   324  			hmacKey:    "key",
   325  			metadata: []metadata.V2Metadata{
   326  				taggedMetadata("key", "banana", "docker.io/user/app"),
   327  				taggedMetadata("key", "orange", "docker.io/user/app"),
   328  				taggedMetadata("key", "plum", "docker.io/user/app"),
   329  			},
   330  			maxExistenceChecks: 3,
   331  			remoteErrors:       map[digest.Digest]error{"orange": distribution.ErrAccessDenied},
   332  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("apple"): {}},
   333  			expectedError:      nil,
   334  			expectedRequests:   []string{"plum", "orange", "banana"},
   335  			expectedRemovals: []metadata.V2Metadata{
   336  				taggedMetadata("key", "plum", "docker.io/user/app"),
   337  				taggedMetadata("key", "banana", "docker.io/user/app"),
   338  			},
   339  		},
   340  		{
   341  			name:       "remove outdated metadata",
   342  			targetRepo: "docker.io/user/app",
   343  			metadata: []metadata.V2Metadata{
   344  				{Digest: digest.Digest("plum"), SourceRepository: "docker.io/library/busybox"},
   345  				{Digest: digest.Digest("orange"), SourceRepository: "docker.io/user/app"},
   346  			},
   347  			maxExistenceChecks: 3,
   348  			remoteErrors:       map[digest.Digest]error{"orange": distribution.ErrBlobUnknown},
   349  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("plum"): {}},
   350  			expectedExists:     false,
   351  			expectedRequests:   []string{"orange"},
   352  			expectedRemovals:   []metadata.V2Metadata{{Digest: digest.Digest("orange"), SourceRepository: "docker.io/user/app"}},
   353  		},
   354  		{
   355  			name:       "missing SourceRepository",
   356  			targetRepo: "busybox",
   357  			metadata: []metadata.V2Metadata{
   358  				{Digest: digest.Digest("1")},
   359  				{Digest: digest.Digest("3")},
   360  				{Digest: digest.Digest("2")},
   361  			},
   362  			maxExistenceChecks: 3,
   363  			expectedExists:     false,
   364  			expectedRequests:   []string{"2", "3", "1"},
   365  		},
   366  
   367  		{
   368  			name:       "with and without SourceRepository",
   369  			targetRepo: "busybox",
   370  			metadata: []metadata.V2Metadata{
   371  				{Digest: digest.Digest("1")},
   372  				{Digest: digest.Digest("2"), SourceRepository: "docker.io/library/busybox"},
   373  				{Digest: digest.Digest("3")},
   374  			},
   375  			remoteBlobs:        map[digest.Digest]distribution.Descriptor{digest.Digest("1"): {Digest: digest.Digest("1")}},
   376  			maxExistenceChecks: 3,
   377  			expectedDescriptor: distribution.Descriptor{Digest: digest.Digest("1"), MediaType: schema2.MediaTypeLayer},
   378  			expectedExists:     true,
   379  			expectedRequests:   []string{"2", "3", "1"},
   380  			expectedAdditions:  []metadata.V2Metadata{{Digest: digest.Digest("1"), SourceRepository: "docker.io/library/busybox"}},
   381  			expectedRemovals: []metadata.V2Metadata{
   382  				{Digest: digest.Digest("2"), SourceRepository: "docker.io/library/busybox"},
   383  			},
   384  		},
   385  	} {
   386  		repoInfo, err := reference.ParseNormalizedNamed(tc.targetRepo)
   387  		if err != nil {
   388  			t.Fatalf("[%s] failed to parse reference name: %v", tc.name, err)
   389  		}
   390  		repo := &mockRepo{
   391  			t:        t,
   392  			errors:   tc.remoteErrors,
   393  			blobs:    tc.remoteBlobs,
   394  			requests: []string{},
   395  		}
   396  		ctx := context.Background()
   397  		ms := &mockV2MetadataService{}
   398  		pd := &v2PushDescriptor{
   399  			hmacKey:  []byte(tc.hmacKey),
   400  			repoInfo: repoInfo,
   401  			layer: &storeLayer{
   402  				Layer: layer.EmptyLayer,
   403  			},
   404  			repo:              repo,
   405  			v2MetadataService: ms,
   406  			pushState:         &pushState{remoteLayers: make(map[layer.DiffID]distribution.Descriptor)},
   407  			checkedDigests:    make(map[digest.Digest]struct{}),
   408  		}
   409  
   410  		desc, exists, err := pd.layerAlreadyExists(ctx, &progressSink{t}, layer.EmptyLayer.DiffID(), tc.checkOtherRepositories, tc.maxExistenceChecks, tc.metadata)
   411  
   412  		if !reflect.DeepEqual(desc, tc.expectedDescriptor) {
   413  			t.Errorf("[%s] got unexpected descriptor: %#+v != %#+v", tc.name, desc, tc.expectedDescriptor)
   414  		}
   415  		if exists != tc.expectedExists {
   416  			t.Errorf("[%s] got unexpected exists: %t != %t", tc.name, exists, tc.expectedExists)
   417  		}
   418  		if !reflect.DeepEqual(err, tc.expectedError) {
   419  			t.Errorf("[%s] got unexpected error: %#+v != %#+v", tc.name, err, tc.expectedError)
   420  		}
   421  
   422  		if len(repo.requests) != len(tc.expectedRequests) {
   423  			t.Errorf("[%s] got unexpected number of requests: %d != %d", tc.name, len(repo.requests), len(tc.expectedRequests))
   424  		}
   425  		for i := 0; i < len(repo.requests) && i < len(tc.expectedRequests); i++ {
   426  			if repo.requests[i] != tc.expectedRequests[i] {
   427  				t.Errorf("[%s] request %d does not match expected: %q != %q", tc.name, i, repo.requests[i], tc.expectedRequests[i])
   428  			}
   429  		}
   430  		for i := len(repo.requests); i < len(tc.expectedRequests); i++ {
   431  			t.Errorf("[%s] missing expected request at position %d (%q)", tc.name, i, tc.expectedRequests[i])
   432  		}
   433  		for i := len(tc.expectedRequests); i < len(repo.requests); i++ {
   434  			t.Errorf("[%s] got unexpected request at position %d (%q)", tc.name, i, repo.requests[i])
   435  		}
   436  
   437  		if len(ms.added) != len(tc.expectedAdditions) {
   438  			t.Errorf("[%s] got unexpected number of additions: %d != %d", tc.name, len(ms.added), len(tc.expectedAdditions))
   439  		}
   440  		for i := 0; i < len(ms.added) && i < len(tc.expectedAdditions); i++ {
   441  			if ms.added[i] != tc.expectedAdditions[i] {
   442  				t.Errorf("[%s] added metadata at %d does not match expected: %q != %q", tc.name, i, ms.added[i], tc.expectedAdditions[i])
   443  			}
   444  		}
   445  		for i := len(ms.added); i < len(tc.expectedAdditions); i++ {
   446  			t.Errorf("[%s] missing expected addition at position %d (%q)", tc.name, i, tc.expectedAdditions[i])
   447  		}
   448  		for i := len(tc.expectedAdditions); i < len(ms.added); i++ {
   449  			t.Errorf("[%s] unexpected metadata addition at position %d (%q)", tc.name, i, ms.added[i])
   450  		}
   451  
   452  		if len(ms.removed) != len(tc.expectedRemovals) {
   453  			t.Errorf("[%s] got unexpected number of removals: %d != %d", tc.name, len(ms.removed), len(tc.expectedRemovals))
   454  		}
   455  		for i := 0; i < len(ms.removed) && i < len(tc.expectedRemovals); i++ {
   456  			if ms.removed[i] != tc.expectedRemovals[i] {
   457  				t.Errorf("[%s] removed metadata at %d does not match expected: %q != %q", tc.name, i, ms.removed[i], tc.expectedRemovals[i])
   458  			}
   459  		}
   460  		for i := len(ms.removed); i < len(tc.expectedRemovals); i++ {
   461  			t.Errorf("[%s] missing expected removal at position %d (%q)", tc.name, i, tc.expectedRemovals[i])
   462  		}
   463  		for i := len(tc.expectedRemovals); i < len(ms.removed); i++ {
   464  			t.Errorf("[%s] removed unexpected metadata at position %d (%q)", tc.name, i, ms.removed[i])
   465  		}
   466  	}
   467  }
   468  
   469  type mockReferenceStore struct {
   470  }
   471  
   472  func (s *mockReferenceStore) References(id digest.Digest) []reference.Named {
   473  	return []reference.Named{}
   474  }
   475  func (s *mockReferenceStore) ReferencesByName(ref reference.Named) []refstore.Association {
   476  	return []refstore.Association{}
   477  }
   478  func (s *mockReferenceStore) AddTag(ref reference.Named, id digest.Digest, force bool) error {
   479  	return nil
   480  }
   481  func (s *mockReferenceStore) AddDigest(ref reference.Canonical, id digest.Digest, force bool) error {
   482  	return nil
   483  }
   484  func (s *mockReferenceStore) Delete(ref reference.Named) (bool, error) {
   485  	return true, nil
   486  }
   487  func (s *mockReferenceStore) Get(ref reference.Named) (digest.Digest, error) {
   488  	return "", nil
   489  }
   490  
   491  func TestWhenEmptyAuthConfig(t *testing.T) {
   492  	for _, authInfo := range []struct {
   493  		username      string
   494  		password      string
   495  		registryToken string
   496  		expected      bool
   497  	}{
   498  		{
   499  			username:      "",
   500  			password:      "",
   501  			registryToken: "",
   502  			expected:      false,
   503  		},
   504  		{
   505  			username:      "username",
   506  			password:      "password",
   507  			registryToken: "",
   508  			expected:      true,
   509  		},
   510  		{
   511  			username:      "",
   512  			password:      "",
   513  			registryToken: "token",
   514  			expected:      true,
   515  		},
   516  	} {
   517  		imagePushConfig := &ImagePushConfig{}
   518  		imagePushConfig.AuthConfig = &types.AuthConfig{
   519  			Username:      authInfo.username,
   520  			Password:      authInfo.password,
   521  			RegistryToken: authInfo.registryToken,
   522  		}
   523  		imagePushConfig.ReferenceStore = &mockReferenceStore{}
   524  		repoInfo, _ := reference.ParseNormalizedNamed("xujihui1985/test.img")
   525  		pusher := &v2Pusher{
   526  			config: imagePushConfig,
   527  			repoInfo: &registry.RepositoryInfo{
   528  				Name: repoInfo,
   529  			},
   530  			endpoint: registry.APIEndpoint{
   531  				URL: &url.URL{
   532  					Scheme: "https",
   533  					Host:   "index.docker.io",
   534  				},
   535  				Version:      registry.APIVersion1,
   536  				TrimHostname: true,
   537  			},
   538  		}
   539  		pusher.Push(context.Background())
   540  		if pusher.pushState.hasAuthInfo != authInfo.expected {
   541  			t.Errorf("hasAuthInfo does not match expected: %t != %t", authInfo.expected, pusher.pushState.hasAuthInfo)
   542  		}
   543  	}
   544  }
   545  
   546  type mockBlobStoreWithCreate struct {
   547  	mockBlobStore
   548  	repo *mockRepoWithBlob
   549  }
   550  
   551  func (blob *mockBlobStoreWithCreate) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
   552  	return nil, errcode.Errors([]error{errcode.ErrorCodeUnauthorized.WithMessage("unauthorized")})
   553  }
   554  
   555  type mockRepoWithBlob struct {
   556  	mockRepo
   557  }
   558  
   559  func (m *mockRepoWithBlob) Blobs(ctx context.Context) distribution.BlobStore {
   560  	blob := &mockBlobStoreWithCreate{}
   561  	blob.mockBlobStore.repo = &m.mockRepo
   562  	blob.repo = m
   563  	return blob
   564  }
   565  
   566  type mockMetadataService struct {
   567  	mockV2MetadataService
   568  }
   569  
   570  func (m *mockMetadataService) GetMetadata(diffID layer.DiffID) ([]metadata.V2Metadata, error) {
   571  	return []metadata.V2Metadata{
   572  		taggedMetadata("abcd", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e28", "docker.io/user/app1"),
   573  		taggedMetadata("abcd", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e22", "docker.io/user/app/base"),
   574  		taggedMetadata("hash", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e23", "docker.io/user/app"),
   575  		taggedMetadata("abcd", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e24", "127.0.0.1/user/app"),
   576  		taggedMetadata("hash", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e25", "docker.io/user/foo"),
   577  		taggedMetadata("hash", "sha256:ff3a5c916c92643ff77519ffa742d3ec61b7f591b6b7504599d95a4a41134e26", "docker.io/app/bar"),
   578  	}, nil
   579  }
   580  
   581  var removeMetadata bool
   582  
   583  func (m *mockMetadataService) Remove(metadata metadata.V2Metadata) error {
   584  	removeMetadata = true
   585  	return nil
   586  }
   587  
   588  func TestPushRegistryWhenAuthInfoEmpty(t *testing.T) {
   589  	repoInfo, _ := reference.ParseNormalizedNamed("user/app")
   590  	ms := &mockMetadataService{}
   591  	remoteErrors := map[digest.Digest]error{digest.Digest("sha256:apple"): distribution.ErrAccessDenied}
   592  	remoteBlobs := map[digest.Digest]distribution.Descriptor{digest.Digest("sha256:apple"): {Digest: digest.Digest("shar256:apple")}}
   593  	repo := &mockRepoWithBlob{
   594  		mockRepo: mockRepo{
   595  			t:        t,
   596  			errors:   remoteErrors,
   597  			blobs:    remoteBlobs,
   598  			requests: []string{},
   599  		},
   600  	}
   601  	pd := &v2PushDescriptor{
   602  		hmacKey:  []byte("abcd"),
   603  		repoInfo: repoInfo,
   604  		layer: &storeLayer{
   605  			Layer: layer.EmptyLayer,
   606  		},
   607  		repo:              repo,
   608  		v2MetadataService: ms,
   609  		pushState: &pushState{
   610  			remoteLayers: make(map[layer.DiffID]distribution.Descriptor),
   611  			hasAuthInfo:  false,
   612  		},
   613  		checkedDigests: make(map[digest.Digest]struct{}),
   614  	}
   615  	pd.Upload(context.Background(), &progressSink{t})
   616  	if removeMetadata {
   617  		t.Fatalf("expect remove not be called but called")
   618  	}
   619  }
   620  
   621  func taggedMetadata(key string, dgst string, sourceRepo string) metadata.V2Metadata {
   622  	meta := metadata.V2Metadata{
   623  		Digest:           digest.Digest(dgst),
   624  		SourceRepository: sourceRepo,
   625  	}
   626  
   627  	meta.HMAC = metadata.ComputeV2MetadataHMAC([]byte(key), &meta)
   628  	return meta
   629  }
   630  
   631  type mockRepo struct {
   632  	t        *testing.T
   633  	errors   map[digest.Digest]error
   634  	blobs    map[digest.Digest]distribution.Descriptor
   635  	requests []string
   636  }
   637  
   638  var _ distribution.Repository = &mockRepo{}
   639  
   640  func (m *mockRepo) Named() reference.Named {
   641  	m.t.Fatalf("Named() not implemented")
   642  	return nil
   643  }
   644  func (m *mockRepo) Manifests(ctc context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
   645  	m.t.Fatalf("Manifests() not implemented")
   646  	return nil, nil
   647  }
   648  func (m *mockRepo) Tags(ctc context.Context) distribution.TagService {
   649  	m.t.Fatalf("Tags() not implemented")
   650  	return nil
   651  }
   652  func (m *mockRepo) Blobs(ctx context.Context) distribution.BlobStore {
   653  	return &mockBlobStore{
   654  		repo: m,
   655  	}
   656  }
   657  
   658  type mockBlobStore struct {
   659  	repo *mockRepo
   660  }
   661  
   662  var _ distribution.BlobStore = &mockBlobStore{}
   663  
   664  func (m *mockBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   665  	m.repo.requests = append(m.repo.requests, dgst.String())
   666  	if err, exists := m.repo.errors[dgst]; exists {
   667  		return distribution.Descriptor{}, err
   668  	}
   669  	if desc, exists := m.repo.blobs[dgst]; exists {
   670  		return desc, nil
   671  	}
   672  	return distribution.Descriptor{}, distribution.ErrBlobUnknown
   673  }
   674  func (m *mockBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
   675  	m.repo.t.Fatal("Get() not implemented")
   676  	return nil, nil
   677  }
   678  
   679  func (m *mockBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
   680  	m.repo.t.Fatal("Open() not implemented")
   681  	return nil, nil
   682  }
   683  
   684  func (m *mockBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
   685  	m.repo.t.Fatal("Put() not implemented")
   686  	return distribution.Descriptor{}, nil
   687  }
   688  
   689  func (m *mockBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
   690  	m.repo.t.Fatal("Create() not implemented")
   691  	return nil, nil
   692  }
   693  func (m *mockBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
   694  	m.repo.t.Fatal("Resume() not implemented")
   695  	return nil, nil
   696  }
   697  func (m *mockBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
   698  	m.repo.t.Fatal("Delete() not implemented")
   699  	return nil
   700  }
   701  func (m *mockBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
   702  	m.repo.t.Fatalf("ServeBlob() not implemented")
   703  	return nil
   704  }
   705  
   706  type mockV2MetadataService struct {
   707  	added   []metadata.V2Metadata
   708  	removed []metadata.V2Metadata
   709  }
   710  
   711  var _ metadata.V2MetadataService = &mockV2MetadataService{}
   712  
   713  func (*mockV2MetadataService) GetMetadata(diffID layer.DiffID) ([]metadata.V2Metadata, error) {
   714  	return nil, nil
   715  }
   716  func (*mockV2MetadataService) GetDiffID(dgst digest.Digest) (layer.DiffID, error) {
   717  	return "", nil
   718  }
   719  func (m *mockV2MetadataService) Add(diffID layer.DiffID, metadata metadata.V2Metadata) error {
   720  	m.added = append(m.added, metadata)
   721  	return nil
   722  }
   723  func (m *mockV2MetadataService) TagAndAdd(diffID layer.DiffID, hmacKey []byte, meta metadata.V2Metadata) error {
   724  	meta.HMAC = metadata.ComputeV2MetadataHMAC(hmacKey, &meta)
   725  	m.Add(diffID, meta)
   726  	return nil
   727  }
   728  func (m *mockV2MetadataService) Remove(metadata metadata.V2Metadata) error {
   729  	m.removed = append(m.removed, metadata)
   730  	return nil
   731  }
   732  
   733  type progressSink struct {
   734  	t *testing.T
   735  }
   736  
   737  func (s *progressSink) WriteProgress(p progress.Progress) error {
   738  	s.t.Logf("progress update: %#+v", p)
   739  	return nil
   740  }