github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/client/repository_test.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strconv"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/docker/distribution"
    17  	"github.com/docker/distribution/context"
    18  	"github.com/docker/distribution/digest"
    19  	"github.com/docker/distribution/manifest"
    20  	"github.com/docker/distribution/manifest/schema1"
    21  	"github.com/docker/distribution/reference"
    22  	"github.com/docker/distribution/registry/api/errcode"
    23  	"github.com/docker/distribution/testutil"
    24  	"github.com/docker/distribution/uuid"
    25  	"github.com/docker/libtrust"
    26  )
    27  
    28  func testServer(rrm testutil.RequestResponseMap) (string, func()) {
    29  	h := testutil.NewHandler(rrm)
    30  	s := httptest.NewServer(h)
    31  	return s.URL, s.Close
    32  }
    33  
    34  func newRandomBlob(size int) (digest.Digest, []byte) {
    35  	b := make([]byte, size)
    36  	if n, err := rand.Read(b); err != nil {
    37  		panic(err)
    38  	} else if n != size {
    39  		panic("unable to read enough bytes")
    40  	}
    41  
    42  	return digest.FromBytes(b), b
    43  }
    44  
    45  func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
    46  	*m = append(*m, testutil.RequestResponseMapping{
    47  		Request: testutil.Request{
    48  			Method: "GET",
    49  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    50  		},
    51  		Response: testutil.Response{
    52  			StatusCode: http.StatusOK,
    53  			Body:       content,
    54  			Headers: http.Header(map[string][]string{
    55  				"Content-Length": {fmt.Sprint(len(content))},
    56  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    57  			}),
    58  		},
    59  	})
    60  
    61  	*m = append(*m, testutil.RequestResponseMapping{
    62  		Request: testutil.Request{
    63  			Method: "HEAD",
    64  			Route:  "/v2/" + repo + "/blobs/" + dgst.String(),
    65  		},
    66  		Response: testutil.Response{
    67  			StatusCode: http.StatusOK,
    68  			Headers: http.Header(map[string][]string{
    69  				"Content-Length": {fmt.Sprint(len(content))},
    70  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
    71  			}),
    72  		},
    73  	})
    74  }
    75  
    76  func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
    77  	headers := map[string][]string{
    78  		"Content-Length": {strconv.Itoa(len(content))},
    79  		"Content-Type":   {"application/json; charset=utf-8"},
    80  	}
    81  	if link != "" {
    82  		headers["Link"] = append(headers["Link"], link)
    83  	}
    84  
    85  	*m = append(*m, testutil.RequestResponseMapping{
    86  		Request: testutil.Request{
    87  			Method: "GET",
    88  			Route:  route,
    89  		},
    90  		Response: testutil.Response{
    91  			StatusCode: http.StatusOK,
    92  			Body:       content,
    93  			Headers:    http.Header(headers),
    94  		},
    95  	})
    96  }
    97  
    98  func TestBlobDelete(t *testing.T) {
    99  	dgst, _ := newRandomBlob(1024)
   100  	var m testutil.RequestResponseMap
   101  	repo, _ := reference.ParseNamed("test.example.com/repo1")
   102  	m = append(m, testutil.RequestResponseMapping{
   103  		Request: testutil.Request{
   104  			Method: "DELETE",
   105  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   106  		},
   107  		Response: testutil.Response{
   108  			StatusCode: http.StatusAccepted,
   109  			Headers: http.Header(map[string][]string{
   110  				"Content-Length": {"0"},
   111  			}),
   112  		},
   113  	})
   114  
   115  	e, c := testServer(m)
   116  	defer c()
   117  
   118  	ctx := context.Background()
   119  	r, err := NewRepository(ctx, repo, e, nil)
   120  	if err != nil {
   121  		t.Fatal(err)
   122  	}
   123  	l := r.Blobs(ctx)
   124  	err = l.Delete(ctx, dgst)
   125  	if err != nil {
   126  		t.Errorf("Error deleting blob: %s", err.Error())
   127  	}
   128  
   129  }
   130  
   131  func TestBlobFetch(t *testing.T) {
   132  	d1, b1 := newRandomBlob(1024)
   133  	var m testutil.RequestResponseMap
   134  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   135  
   136  	e, c := testServer(m)
   137  	defer c()
   138  
   139  	ctx := context.Background()
   140  	repo, _ := reference.ParseNamed("test.example.com/repo1")
   141  	r, err := NewRepository(ctx, repo, e, nil)
   142  	if err != nil {
   143  		t.Fatal(err)
   144  	}
   145  	l := r.Blobs(ctx)
   146  
   147  	b, err := l.Get(ctx, d1)
   148  	if err != nil {
   149  		t.Fatal(err)
   150  	}
   151  	if bytes.Compare(b, b1) != 0 {
   152  		t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
   153  	}
   154  
   155  	// TODO(dmcgowan): Test for unknown blob case
   156  }
   157  
   158  func TestBlobExistsNoContentLength(t *testing.T) {
   159  	var m testutil.RequestResponseMap
   160  
   161  	repo, _ := reference.ParseNamed("biff")
   162  	dgst, content := newRandomBlob(1024)
   163  	m = append(m, testutil.RequestResponseMapping{
   164  		Request: testutil.Request{
   165  			Method: "GET",
   166  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   167  		},
   168  		Response: testutil.Response{
   169  			StatusCode: http.StatusOK,
   170  			Body:       content,
   171  			Headers: http.Header(map[string][]string{
   172  				//			"Content-Length": {fmt.Sprint(len(content))},
   173  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   174  			}),
   175  		},
   176  	})
   177  
   178  	m = append(m, testutil.RequestResponseMapping{
   179  		Request: testutil.Request{
   180  			Method: "HEAD",
   181  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   182  		},
   183  		Response: testutil.Response{
   184  			StatusCode: http.StatusOK,
   185  			Headers: http.Header(map[string][]string{
   186  				//			"Content-Length": {fmt.Sprint(len(content))},
   187  				"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   188  			}),
   189  		},
   190  	})
   191  	e, c := testServer(m)
   192  	defer c()
   193  
   194  	ctx := context.Background()
   195  	r, err := NewRepository(ctx, repo, e, nil)
   196  	if err != nil {
   197  		t.Fatal(err)
   198  	}
   199  	l := r.Blobs(ctx)
   200  
   201  	_, err = l.Stat(ctx, dgst)
   202  	if err == nil {
   203  		t.Fatal(err)
   204  	}
   205  	if !strings.Contains(err.Error(), "missing content-length heade") {
   206  		t.Fatalf("Expected missing content-length error message")
   207  	}
   208  
   209  }
   210  
   211  func TestBlobExists(t *testing.T) {
   212  	d1, b1 := newRandomBlob(1024)
   213  	var m testutil.RequestResponseMap
   214  	addTestFetch("test.example.com/repo1", d1, b1, &m)
   215  
   216  	e, c := testServer(m)
   217  	defer c()
   218  
   219  	ctx := context.Background()
   220  	repo, _ := reference.ParseNamed("test.example.com/repo1")
   221  	r, err := NewRepository(ctx, repo, e, nil)
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  	l := r.Blobs(ctx)
   226  
   227  	stat, err := l.Stat(ctx, d1)
   228  	if err != nil {
   229  		t.Fatal(err)
   230  	}
   231  
   232  	if stat.Digest != d1 {
   233  		t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1)
   234  	}
   235  
   236  	if stat.Size != int64(len(b1)) {
   237  		t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1))
   238  	}
   239  
   240  	// TODO(dmcgowan): Test error cases and ErrBlobUnknown case
   241  }
   242  
   243  func TestBlobUploadChunked(t *testing.T) {
   244  	dgst, b1 := newRandomBlob(1024)
   245  	var m testutil.RequestResponseMap
   246  	chunks := [][]byte{
   247  		b1[0:256],
   248  		b1[256:512],
   249  		b1[512:513],
   250  		b1[513:1024],
   251  	}
   252  	repo, _ := reference.ParseNamed("test.example.com/uploadrepo")
   253  	uuids := []string{uuid.Generate().String()}
   254  	m = append(m, testutil.RequestResponseMapping{
   255  		Request: testutil.Request{
   256  			Method: "POST",
   257  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/",
   258  		},
   259  		Response: testutil.Response{
   260  			StatusCode: http.StatusAccepted,
   261  			Headers: http.Header(map[string][]string{
   262  				"Content-Length":     {"0"},
   263  				"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[0]},
   264  				"Docker-Upload-UUID": {uuids[0]},
   265  				"Range":              {"0-0"},
   266  			}),
   267  		},
   268  	})
   269  	offset := 0
   270  	for i, chunk := range chunks {
   271  		uuids = append(uuids, uuid.Generate().String())
   272  		newOffset := offset + len(chunk)
   273  		m = append(m, testutil.RequestResponseMapping{
   274  			Request: testutil.Request{
   275  				Method: "PATCH",
   276  				Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i],
   277  				Body:   chunk,
   278  			},
   279  			Response: testutil.Response{
   280  				StatusCode: http.StatusAccepted,
   281  				Headers: http.Header(map[string][]string{
   282  					"Content-Length":     {"0"},
   283  					"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i+1]},
   284  					"Docker-Upload-UUID": {uuids[i+1]},
   285  					"Range":              {fmt.Sprintf("%d-%d", offset, newOffset-1)},
   286  				}),
   287  			},
   288  		})
   289  		offset = newOffset
   290  	}
   291  	m = append(m, testutil.RequestResponseMapping{
   292  		Request: testutil.Request{
   293  			Method: "PUT",
   294  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[len(uuids)-1],
   295  			QueryParams: map[string][]string{
   296  				"digest": {dgst.String()},
   297  			},
   298  		},
   299  		Response: testutil.Response{
   300  			StatusCode: http.StatusCreated,
   301  			Headers: http.Header(map[string][]string{
   302  				"Content-Length":        {"0"},
   303  				"Docker-Content-Digest": {dgst.String()},
   304  				"Content-Range":         {fmt.Sprintf("0-%d", offset-1)},
   305  			}),
   306  		},
   307  	})
   308  	m = append(m, testutil.RequestResponseMapping{
   309  		Request: testutil.Request{
   310  			Method: "HEAD",
   311  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   312  		},
   313  		Response: testutil.Response{
   314  			StatusCode: http.StatusOK,
   315  			Headers: http.Header(map[string][]string{
   316  				"Content-Length": {fmt.Sprint(offset)},
   317  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   318  			}),
   319  		},
   320  	})
   321  
   322  	e, c := testServer(m)
   323  	defer c()
   324  
   325  	ctx := context.Background()
   326  	r, err := NewRepository(ctx, repo, e, nil)
   327  	if err != nil {
   328  		t.Fatal(err)
   329  	}
   330  	l := r.Blobs(ctx)
   331  
   332  	upload, err := l.Create(ctx)
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  
   337  	if upload.ID() != uuids[0] {
   338  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0])
   339  	}
   340  
   341  	for _, chunk := range chunks {
   342  		n, err := upload.Write(chunk)
   343  		if err != nil {
   344  			t.Fatal(err)
   345  		}
   346  		if n != len(chunk) {
   347  			t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk))
   348  		}
   349  	}
   350  
   351  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   352  		Digest: dgst,
   353  		Size:   int64(len(b1)),
   354  	})
   355  	if err != nil {
   356  		t.Fatal(err)
   357  	}
   358  
   359  	if blob.Size != int64(len(b1)) {
   360  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   361  	}
   362  }
   363  
   364  func TestBlobUploadMonolithic(t *testing.T) {
   365  	dgst, b1 := newRandomBlob(1024)
   366  	var m testutil.RequestResponseMap
   367  	repo, _ := reference.ParseNamed("test.example.com/uploadrepo")
   368  	uploadID := uuid.Generate().String()
   369  	m = append(m, testutil.RequestResponseMapping{
   370  		Request: testutil.Request{
   371  			Method: "POST",
   372  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/",
   373  		},
   374  		Response: testutil.Response{
   375  			StatusCode: http.StatusAccepted,
   376  			Headers: http.Header(map[string][]string{
   377  				"Content-Length":     {"0"},
   378  				"Location":           {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
   379  				"Docker-Upload-UUID": {uploadID},
   380  				"Range":              {"0-0"},
   381  			}),
   382  		},
   383  	})
   384  	m = append(m, testutil.RequestResponseMapping{
   385  		Request: testutil.Request{
   386  			Method: "PATCH",
   387  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
   388  			Body:   b1,
   389  		},
   390  		Response: testutil.Response{
   391  			StatusCode: http.StatusAccepted,
   392  			Headers: http.Header(map[string][]string{
   393  				"Location":              {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID},
   394  				"Docker-Upload-UUID":    {uploadID},
   395  				"Content-Length":        {"0"},
   396  				"Docker-Content-Digest": {dgst.String()},
   397  				"Range":                 {fmt.Sprintf("0-%d", len(b1)-1)},
   398  			}),
   399  		},
   400  	})
   401  	m = append(m, testutil.RequestResponseMapping{
   402  		Request: testutil.Request{
   403  			Method: "PUT",
   404  			Route:  "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID,
   405  			QueryParams: map[string][]string{
   406  				"digest": {dgst.String()},
   407  			},
   408  		},
   409  		Response: testutil.Response{
   410  			StatusCode: http.StatusCreated,
   411  			Headers: http.Header(map[string][]string{
   412  				"Content-Length":        {"0"},
   413  				"Docker-Content-Digest": {dgst.String()},
   414  				"Content-Range":         {fmt.Sprintf("0-%d", len(b1)-1)},
   415  			}),
   416  		},
   417  	})
   418  	m = append(m, testutil.RequestResponseMapping{
   419  		Request: testutil.Request{
   420  			Method: "HEAD",
   421  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   422  		},
   423  		Response: testutil.Response{
   424  			StatusCode: http.StatusOK,
   425  			Headers: http.Header(map[string][]string{
   426  				"Content-Length": {fmt.Sprint(len(b1))},
   427  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   428  			}),
   429  		},
   430  	})
   431  
   432  	e, c := testServer(m)
   433  	defer c()
   434  
   435  	ctx := context.Background()
   436  	r, err := NewRepository(ctx, repo, e, nil)
   437  	if err != nil {
   438  		t.Fatal(err)
   439  	}
   440  	l := r.Blobs(ctx)
   441  
   442  	upload, err := l.Create(ctx)
   443  	if err != nil {
   444  		t.Fatal(err)
   445  	}
   446  
   447  	if upload.ID() != uploadID {
   448  		log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
   449  	}
   450  
   451  	n, err := upload.ReadFrom(bytes.NewReader(b1))
   452  	if err != nil {
   453  		t.Fatal(err)
   454  	}
   455  	if n != int64(len(b1)) {
   456  		t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
   457  	}
   458  
   459  	blob, err := upload.Commit(ctx, distribution.Descriptor{
   460  		Digest: dgst,
   461  		Size:   int64(len(b1)),
   462  	})
   463  	if err != nil {
   464  		t.Fatal(err)
   465  	}
   466  
   467  	if blob.Size != int64(len(b1)) {
   468  		t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
   469  	}
   470  }
   471  
   472  func TestBlobMount(t *testing.T) {
   473  	dgst, content := newRandomBlob(1024)
   474  	var m testutil.RequestResponseMap
   475  	repo, _ := reference.ParseNamed("test.example.com/uploadrepo")
   476  
   477  	sourceRepo, _ := reference.ParseNamed("test.example.com/sourcerepo")
   478  	canonicalRef, _ := reference.WithDigest(sourceRepo, dgst)
   479  
   480  	m = append(m, testutil.RequestResponseMapping{
   481  		Request: testutil.Request{
   482  			Method:      "POST",
   483  			Route:       "/v2/" + repo.Name() + "/blobs/uploads/",
   484  			QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}},
   485  		},
   486  		Response: testutil.Response{
   487  			StatusCode: http.StatusCreated,
   488  			Headers: http.Header(map[string][]string{
   489  				"Content-Length":        {"0"},
   490  				"Location":              {"/v2/" + repo.Name() + "/blobs/" + dgst.String()},
   491  				"Docker-Content-Digest": {dgst.String()},
   492  			}),
   493  		},
   494  	})
   495  	m = append(m, testutil.RequestResponseMapping{
   496  		Request: testutil.Request{
   497  			Method: "HEAD",
   498  			Route:  "/v2/" + repo.Name() + "/blobs/" + dgst.String(),
   499  		},
   500  		Response: testutil.Response{
   501  			StatusCode: http.StatusOK,
   502  			Headers: http.Header(map[string][]string{
   503  				"Content-Length": {fmt.Sprint(len(content))},
   504  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   505  			}),
   506  		},
   507  	})
   508  
   509  	e, c := testServer(m)
   510  	defer c()
   511  
   512  	ctx := context.Background()
   513  	r, err := NewRepository(ctx, repo, e, nil)
   514  	if err != nil {
   515  		t.Fatal(err)
   516  	}
   517  
   518  	l := r.Blobs(ctx)
   519  
   520  	bw, err := l.Create(ctx, WithMountFrom(canonicalRef))
   521  	if bw != nil {
   522  		t.Fatalf("Expected blob writer to be nil, was %v", bw)
   523  	}
   524  
   525  	if ebm, ok := err.(distribution.ErrBlobMounted); ok {
   526  		if ebm.From.Digest() != dgst {
   527  			t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst)
   528  		}
   529  		if ebm.From.Name() != sourceRepo.Name() {
   530  			t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo)
   531  		}
   532  	} else {
   533  		t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err)
   534  	}
   535  }
   536  
   537  func newRandomSchemaV1Manifest(name reference.Named, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
   538  	blobs := make([]schema1.FSLayer, blobCount)
   539  	history := make([]schema1.History, blobCount)
   540  
   541  	for i := 0; i < blobCount; i++ {
   542  		dgst, blob := newRandomBlob((i % 5) * 16)
   543  
   544  		blobs[i] = schema1.FSLayer{BlobSum: dgst}
   545  		history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)}
   546  	}
   547  
   548  	m := schema1.Manifest{
   549  		Name:         name.String(),
   550  		Tag:          tag,
   551  		Architecture: "x86",
   552  		FSLayers:     blobs,
   553  		History:      history,
   554  		Versioned: manifest.Versioned{
   555  			SchemaVersion: 1,
   556  		},
   557  	}
   558  
   559  	pk, err := libtrust.GenerateECP256PrivateKey()
   560  	if err != nil {
   561  		panic(err)
   562  	}
   563  
   564  	sm, err := schema1.Sign(&m, pk)
   565  	if err != nil {
   566  		panic(err)
   567  	}
   568  
   569  	return sm, digest.FromBytes(sm.Canonical), sm.Canonical
   570  }
   571  
   572  func addTestManifestWithEtag(repo reference.Named, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) {
   573  	actualDigest := digest.FromBytes(content)
   574  	getReqWithEtag := testutil.Request{
   575  		Method: "GET",
   576  		Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   577  		Headers: http.Header(map[string][]string{
   578  			"If-None-Match": {fmt.Sprintf(`"%s"`, dgst)},
   579  		}),
   580  	}
   581  
   582  	var getRespWithEtag testutil.Response
   583  	if actualDigest.String() == dgst {
   584  		getRespWithEtag = testutil.Response{
   585  			StatusCode: http.StatusNotModified,
   586  			Body:       []byte{},
   587  			Headers: http.Header(map[string][]string{
   588  				"Content-Length": {"0"},
   589  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   590  				"Content-Type":   {schema1.MediaTypeSignedManifest},
   591  			}),
   592  		}
   593  	} else {
   594  		getRespWithEtag = testutil.Response{
   595  			StatusCode: http.StatusOK,
   596  			Body:       content,
   597  			Headers: http.Header(map[string][]string{
   598  				"Content-Length": {fmt.Sprint(len(content))},
   599  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   600  				"Content-Type":   {schema1.MediaTypeSignedManifest},
   601  			}),
   602  		}
   603  
   604  	}
   605  	*m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag})
   606  }
   607  
   608  func addTestManifest(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) {
   609  	*m = append(*m, testutil.RequestResponseMapping{
   610  		Request: testutil.Request{
   611  			Method: "GET",
   612  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   613  		},
   614  		Response: testutil.Response{
   615  			StatusCode: http.StatusOK,
   616  			Body:       content,
   617  			Headers: http.Header(map[string][]string{
   618  				"Content-Length": {fmt.Sprint(len(content))},
   619  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   620  				"Content-Type":   {mediatype},
   621  			}),
   622  		},
   623  	})
   624  	*m = append(*m, testutil.RequestResponseMapping{
   625  		Request: testutil.Request{
   626  			Method: "HEAD",
   627  			Route:  "/v2/" + repo.Name() + "/manifests/" + reference,
   628  		},
   629  		Response: testutil.Response{
   630  			StatusCode: http.StatusOK,
   631  			Headers: http.Header(map[string][]string{
   632  				"Content-Length": {fmt.Sprint(len(content))},
   633  				"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   634  				"Content-Type":   {mediatype},
   635  			}),
   636  		},
   637  	})
   638  
   639  }
   640  
   641  func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
   642  	if m1.Name != m2.Name {
   643  		return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name)
   644  	}
   645  	if m1.Tag != m2.Tag {
   646  		return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag)
   647  	}
   648  	if len(m1.FSLayers) != len(m2.FSLayers) {
   649  		return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers))
   650  	}
   651  	for i := range m1.FSLayers {
   652  		if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum {
   653  			return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum)
   654  		}
   655  	}
   656  	if len(m1.History) != len(m2.History) {
   657  		return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History))
   658  	}
   659  	for i := range m1.History {
   660  		if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility {
   661  			return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility)
   662  		}
   663  	}
   664  	return nil
   665  }
   666  
   667  func TestV1ManifestFetch(t *testing.T) {
   668  	ctx := context.Background()
   669  	repo, _ := reference.ParseNamed("test.example.com/repo")
   670  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   671  	var m testutil.RequestResponseMap
   672  	_, pl, err := m1.Payload()
   673  	if err != nil {
   674  		t.Fatal(err)
   675  	}
   676  	addTestManifest(repo, dgst.String(), schema1.MediaTypeSignedManifest, pl, &m)
   677  	addTestManifest(repo, "latest", schema1.MediaTypeSignedManifest, pl, &m)
   678  	addTestManifest(repo, "badcontenttype", "text/html", pl, &m)
   679  
   680  	e, c := testServer(m)
   681  	defer c()
   682  
   683  	r, err := NewRepository(context.Background(), repo, e, nil)
   684  	if err != nil {
   685  		t.Fatal(err)
   686  	}
   687  	ms, err := r.Manifests(ctx)
   688  	if err != nil {
   689  		t.Fatal(err)
   690  	}
   691  
   692  	ok, err := ms.Exists(ctx, dgst)
   693  	if err != nil {
   694  		t.Fatal(err)
   695  	}
   696  	if !ok {
   697  		t.Fatal("Manifest does not exist")
   698  	}
   699  
   700  	manifest, err := ms.Get(ctx, dgst)
   701  	if err != nil {
   702  		t.Fatal(err)
   703  	}
   704  	v1manifest, ok := manifest.(*schema1.SignedManifest)
   705  	if !ok {
   706  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   707  	}
   708  
   709  	if err := checkEqualManifest(v1manifest, m1); err != nil {
   710  		t.Fatal(err)
   711  	}
   712  
   713  	manifest, err = ms.Get(ctx, dgst, WithTag("latest"))
   714  	if err != nil {
   715  		t.Fatal(err)
   716  	}
   717  	v1manifest, ok = manifest.(*schema1.SignedManifest)
   718  	if !ok {
   719  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   720  	}
   721  
   722  	if err = checkEqualManifest(v1manifest, m1); err != nil {
   723  		t.Fatal(err)
   724  	}
   725  
   726  	manifest, err = ms.Get(ctx, dgst, WithTag("badcontenttype"))
   727  	if err != nil {
   728  		t.Fatal(err)
   729  	}
   730  	v1manifest, ok = manifest.(*schema1.SignedManifest)
   731  	if !ok {
   732  		t.Fatalf("Unexpected manifest type from Get: %T", manifest)
   733  	}
   734  
   735  	if err = checkEqualManifest(v1manifest, m1); err != nil {
   736  		t.Fatal(err)
   737  	}
   738  }
   739  
   740  func TestManifestFetchWithEtag(t *testing.T) {
   741  	repo, _ := reference.ParseNamed("test.example.com/repo/by/tag")
   742  	_, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
   743  	var m testutil.RequestResponseMap
   744  	addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
   745  
   746  	e, c := testServer(m)
   747  	defer c()
   748  
   749  	ctx := context.Background()
   750  	r, err := NewRepository(ctx, repo, e, nil)
   751  	if err != nil {
   752  		t.Fatal(err)
   753  	}
   754  
   755  	ms, err := r.Manifests(ctx)
   756  	if err != nil {
   757  		t.Fatal(err)
   758  	}
   759  
   760  	clientManifestService, ok := ms.(*manifests)
   761  	if !ok {
   762  		panic("wrong type for client manifest service")
   763  	}
   764  	_, err = clientManifestService.Get(ctx, d1, WithTag("latest"), AddEtagToTag("latest", d1.String()))
   765  	if err != distribution.ErrManifestNotModified {
   766  		t.Fatal(err)
   767  	}
   768  }
   769  
   770  func TestManifestDelete(t *testing.T) {
   771  	repo, _ := reference.ParseNamed("test.example.com/repo/delete")
   772  	_, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   773  	_, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   774  	var m testutil.RequestResponseMap
   775  	m = append(m, testutil.RequestResponseMapping{
   776  		Request: testutil.Request{
   777  			Method: "DELETE",
   778  			Route:  "/v2/" + repo.Name() + "/manifests/" + dgst1.String(),
   779  		},
   780  		Response: testutil.Response{
   781  			StatusCode: http.StatusAccepted,
   782  			Headers: http.Header(map[string][]string{
   783  				"Content-Length": {"0"},
   784  			}),
   785  		},
   786  	})
   787  
   788  	e, c := testServer(m)
   789  	defer c()
   790  
   791  	r, err := NewRepository(context.Background(), repo, e, nil)
   792  	if err != nil {
   793  		t.Fatal(err)
   794  	}
   795  	ctx := context.Background()
   796  	ms, err := r.Manifests(ctx)
   797  	if err != nil {
   798  		t.Fatal(err)
   799  	}
   800  
   801  	if err := ms.Delete(ctx, dgst1); err != nil {
   802  		t.Fatal(err)
   803  	}
   804  	if err := ms.Delete(ctx, dgst2); err == nil {
   805  		t.Fatal("Expected error deleting unknown manifest")
   806  	}
   807  	// TODO(dmcgowan): Check for specific unknown error
   808  }
   809  
   810  func TestManifestPut(t *testing.T) {
   811  	repo, _ := reference.ParseNamed("test.example.com/repo/delete")
   812  	m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
   813  
   814  	_, payload, err := m1.Payload()
   815  	if err != nil {
   816  		t.Fatal(err)
   817  	}
   818  	var m testutil.RequestResponseMap
   819  	m = append(m, testutil.RequestResponseMapping{
   820  		Request: testutil.Request{
   821  			Method: "PUT",
   822  			Route:  "/v2/" + repo.Name() + "/manifests/other",
   823  			Body:   payload,
   824  		},
   825  		Response: testutil.Response{
   826  			StatusCode: http.StatusAccepted,
   827  			Headers: http.Header(map[string][]string{
   828  				"Content-Length":        {"0"},
   829  				"Docker-Content-Digest": {dgst.String()},
   830  			}),
   831  		},
   832  	})
   833  
   834  	e, c := testServer(m)
   835  	defer c()
   836  
   837  	r, err := NewRepository(context.Background(), repo, e, nil)
   838  	if err != nil {
   839  		t.Fatal(err)
   840  	}
   841  	ctx := context.Background()
   842  	ms, err := r.Manifests(ctx)
   843  	if err != nil {
   844  		t.Fatal(err)
   845  	}
   846  
   847  	if _, err := ms.Put(ctx, m1, WithTag(m1.Tag)); err != nil {
   848  		t.Fatal(err)
   849  	}
   850  
   851  	// TODO(dmcgowan): Check for invalid input error
   852  }
   853  
   854  func TestManifestTags(t *testing.T) {
   855  	repo, _ := reference.ParseNamed("test.example.com/repo/tags/list")
   856  	tagsList := []byte(strings.TrimSpace(`
   857  {
   858  	"name": "test.example.com/repo/tags/list",
   859  	"tags": [
   860  		"tag1",
   861  		"tag2",
   862  		"funtag"
   863  	]
   864  }
   865  	`))
   866  	var m testutil.RequestResponseMap
   867  	for i := 0; i < 3; i++ {
   868  		m = append(m, testutil.RequestResponseMapping{
   869  			Request: testutil.Request{
   870  				Method: "GET",
   871  				Route:  "/v2/" + repo.Name() + "/tags/list",
   872  			},
   873  			Response: testutil.Response{
   874  				StatusCode: http.StatusOK,
   875  				Body:       tagsList,
   876  				Headers: http.Header(map[string][]string{
   877  					"Content-Length": {fmt.Sprint(len(tagsList))},
   878  					"Last-Modified":  {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
   879  				}),
   880  			},
   881  		})
   882  	}
   883  	e, c := testServer(m)
   884  	defer c()
   885  
   886  	r, err := NewRepository(context.Background(), repo, e, nil)
   887  	if err != nil {
   888  		t.Fatal(err)
   889  	}
   890  
   891  	ctx := context.Background()
   892  	tagService := r.Tags(ctx)
   893  
   894  	tags, err := tagService.All(ctx)
   895  	if err != nil {
   896  		t.Fatal(err)
   897  	}
   898  	if len(tags) != 3 {
   899  		t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
   900  	}
   901  
   902  	expected := map[string]struct{}{
   903  		"tag1":   {},
   904  		"tag2":   {},
   905  		"funtag": {},
   906  	}
   907  	for _, t := range tags {
   908  		delete(expected, t)
   909  	}
   910  	if len(expected) != 0 {
   911  		t.Fatalf("unexpected tags returned: %v", expected)
   912  	}
   913  	// TODO(dmcgowan): Check for error cases
   914  }
   915  
   916  func TestManifestUnauthorized(t *testing.T) {
   917  	repo, _ := reference.ParseNamed("test.example.com/repo")
   918  	_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
   919  	var m testutil.RequestResponseMap
   920  
   921  	m = append(m, testutil.RequestResponseMapping{
   922  		Request: testutil.Request{
   923  			Method: "GET",
   924  			Route:  "/v2/" + repo.Name() + "/manifests/" + dgst.String(),
   925  		},
   926  		Response: testutil.Response{
   927  			StatusCode: http.StatusUnauthorized,
   928  			Body:       []byte("<html>garbage</html>"),
   929  		},
   930  	})
   931  
   932  	e, c := testServer(m)
   933  	defer c()
   934  
   935  	r, err := NewRepository(context.Background(), repo, e, nil)
   936  	if err != nil {
   937  		t.Fatal(err)
   938  	}
   939  	ctx := context.Background()
   940  	ms, err := r.Manifests(ctx)
   941  	if err != nil {
   942  		t.Fatal(err)
   943  	}
   944  
   945  	_, err = ms.Get(ctx, dgst)
   946  	if err == nil {
   947  		t.Fatal("Expected error fetching manifest")
   948  	}
   949  	v2Err, ok := err.(errcode.Error)
   950  	if !ok {
   951  		t.Fatalf("Unexpected error type: %#v", err)
   952  	}
   953  	if v2Err.Code != errcode.ErrorCodeUnauthorized {
   954  		t.Fatalf("Unexpected error code: %s", v2Err.Code.String())
   955  	}
   956  	if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected {
   957  		t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
   958  	}
   959  }
   960  
   961  func TestCatalog(t *testing.T) {
   962  	var m testutil.RequestResponseMap
   963  	addTestCatalog(
   964  		"/v2/_catalog?n=5",
   965  		[]byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
   966  
   967  	e, c := testServer(m)
   968  	defer c()
   969  
   970  	entries := make([]string, 5)
   971  
   972  	r, err := NewRegistry(context.Background(), e, nil)
   973  	if err != nil {
   974  		t.Fatal(err)
   975  	}
   976  
   977  	ctx := context.Background()
   978  	numFilled, err := r.Repositories(ctx, entries, "")
   979  	if err != io.EOF {
   980  		t.Fatal(err)
   981  	}
   982  
   983  	if numFilled != 3 {
   984  		t.Fatalf("Got wrong number of repos")
   985  	}
   986  }
   987  
   988  func TestCatalogInParts(t *testing.T) {
   989  	var m testutil.RequestResponseMap
   990  	addTestCatalog(
   991  		"/v2/_catalog?n=2",
   992  		[]byte("{\"repositories\":[\"bar\", \"baz\"]}"),
   993  		"</v2/_catalog?last=baz&n=2>", &m)
   994  	addTestCatalog(
   995  		"/v2/_catalog?last=baz&n=2",
   996  		[]byte("{\"repositories\":[\"foo\"]}"),
   997  		"", &m)
   998  
   999  	e, c := testServer(m)
  1000  	defer c()
  1001  
  1002  	entries := make([]string, 2)
  1003  
  1004  	r, err := NewRegistry(context.Background(), e, nil)
  1005  	if err != nil {
  1006  		t.Fatal(err)
  1007  	}
  1008  
  1009  	ctx := context.Background()
  1010  	numFilled, err := r.Repositories(ctx, entries, "")
  1011  	if err != nil {
  1012  		t.Fatal(err)
  1013  	}
  1014  
  1015  	if numFilled != 2 {
  1016  		t.Fatalf("Got wrong number of repos")
  1017  	}
  1018  
  1019  	numFilled, err = r.Repositories(ctx, entries, "baz")
  1020  	if err != io.EOF {
  1021  		t.Fatal(err)
  1022  	}
  1023  
  1024  	if numFilled != 1 {
  1025  		t.Fatalf("Got wrong number of repos")
  1026  	}
  1027  }
  1028  
  1029  func TestSanitizeLocation(t *testing.T) {
  1030  	for _, testcase := range []struct {
  1031  		description string
  1032  		location    string
  1033  		source      string
  1034  		expected    string
  1035  		err         error
  1036  	}{
  1037  		{
  1038  			description: "ensure relative location correctly resolved",
  1039  			location:    "/v2/foo/baasdf",
  1040  			source:      "http://blahalaja.com/v1",
  1041  			expected:    "http://blahalaja.com/v2/foo/baasdf",
  1042  		},
  1043  		{
  1044  			description: "ensure parameters are preserved",
  1045  			location:    "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
  1046  			source:      "http://blahalaja.com/v1",
  1047  			expected:    "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
  1048  		},
  1049  		{
  1050  			description: "ensure new hostname overidden",
  1051  			location:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
  1052  			source:      "http://blahalaja.com/v1",
  1053  			expected:    "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
  1054  		},
  1055  	} {
  1056  		fatalf := func(format string, args ...interface{}) {
  1057  			t.Fatalf(testcase.description+": "+format, args...)
  1058  		}
  1059  
  1060  		s, err := sanitizeLocation(testcase.location, testcase.source)
  1061  		if err != testcase.err {
  1062  			if testcase.err != nil {
  1063  				fatalf("expected error: %v != %v", err, testcase)
  1064  			} else {
  1065  				fatalf("unexpected error sanitizing: %v", err)
  1066  			}
  1067  		}
  1068  
  1069  		if s != testcase.expected {
  1070  			fatalf("bad sanitize: %q != %q", s, testcase.expected)
  1071  		}
  1072  	}
  1073  }