github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/distribution/push_v2_test.go (about)

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