forked from TrueCloudLab/distribution
58232e50cf
The current implementation of digest.FromBytes returns an error. This error can never be non-nil, but its presence in the function signature means each call site needs error handling code for an error that is always nil. I verified that none of the hash.Hash implementations in the standard library can return an error on Write. Nor can any of the hash.Hash implementations vendored in distribution. This commit changes digest.FromBytes not to return an error. If Write returns an error, it will panic, but as discussed above, this should never happen. This commit also avoids using a bytes.Reader to feed data into the hash function in FromBytes. This makes the hypothetical case that would panic a bit more explicit, and should also be more performant. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
952 lines
23 KiB
Go
952 lines
23 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/context"
|
|
"github.com/docker/distribution/digest"
|
|
"github.com/docker/distribution/manifest"
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/docker/distribution/testutil"
|
|
"github.com/docker/distribution/uuid"
|
|
"github.com/docker/libtrust"
|
|
)
|
|
|
|
func testServer(rrm testutil.RequestResponseMap) (string, func()) {
|
|
h := testutil.NewHandler(rrm)
|
|
s := httptest.NewServer(h)
|
|
return s.URL, s.Close
|
|
}
|
|
|
|
func newRandomBlob(size int) (digest.Digest, []byte) {
|
|
b := make([]byte, size)
|
|
if n, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
} else if n != size {
|
|
panic("unable to read enough bytes")
|
|
}
|
|
|
|
return digest.FromBytes(b), b
|
|
}
|
|
|
|
func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) {
|
|
|
|
*m = append(*m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: content,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
*m = append(*m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "HEAD",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
}
|
|
|
|
func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
|
|
headers := map[string][]string{
|
|
"Content-Length": {strconv.Itoa(len(content))},
|
|
"Content-Type": {"application/json; charset=utf-8"},
|
|
}
|
|
if link != "" {
|
|
headers["Link"] = append(headers["Link"], link)
|
|
}
|
|
|
|
*m = append(*m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: route,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: content,
|
|
Headers: http.Header(headers),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBlobDelete(t *testing.T) {
|
|
dgst, _ := newRandomBlob(1024)
|
|
var m testutil.RequestResponseMap
|
|
repo := "test.example.com/repo1"
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "DELETE",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
err = l.Delete(ctx, dgst)
|
|
if err != nil {
|
|
t.Errorf("Error deleting blob: %s", err.Error())
|
|
}
|
|
|
|
}
|
|
|
|
func TestBlobFetch(t *testing.T) {
|
|
d1, b1 := newRandomBlob(1024)
|
|
var m testutil.RequestResponseMap
|
|
addTestFetch("test.example.com/repo1", d1, b1, &m)
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, "test.example.com/repo1", e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
|
|
b, err := l.Get(ctx, d1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bytes.Compare(b, b1) != 0 {
|
|
t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1))
|
|
}
|
|
|
|
// TODO(dmcgowan): Test for unknown blob case
|
|
}
|
|
|
|
func TestBlobExistsNoContentLength(t *testing.T) {
|
|
var m testutil.RequestResponseMap
|
|
|
|
repo := "biff"
|
|
dgst, content := newRandomBlob(1024)
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: content,
|
|
Headers: http.Header(map[string][]string{
|
|
// "Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "HEAD",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Headers: http.Header(map[string][]string{
|
|
// "Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
|
|
_, err = l.Stat(ctx, dgst)
|
|
if err == nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(err.Error(), "missing content-length heade") {
|
|
t.Fatalf("Expected missing content-length error message")
|
|
}
|
|
|
|
}
|
|
|
|
func TestBlobExists(t *testing.T) {
|
|
d1, b1 := newRandomBlob(1024)
|
|
var m testutil.RequestResponseMap
|
|
addTestFetch("test.example.com/repo1", d1, b1, &m)
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, "test.example.com/repo1", e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
|
|
stat, err := l.Stat(ctx, d1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if stat.Digest != d1 {
|
|
t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1)
|
|
}
|
|
|
|
if stat.Size != int64(len(b1)) {
|
|
t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1))
|
|
}
|
|
|
|
// TODO(dmcgowan): Test error cases and ErrBlobUnknown case
|
|
}
|
|
|
|
func TestBlobUploadChunked(t *testing.T) {
|
|
dgst, b1 := newRandomBlob(1024)
|
|
var m testutil.RequestResponseMap
|
|
chunks := [][]byte{
|
|
b1[0:256],
|
|
b1[256:512],
|
|
b1[512:513],
|
|
b1[513:1024],
|
|
}
|
|
repo := "test.example.com/uploadrepo"
|
|
uuids := []string{uuid.Generate().String()}
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "POST",
|
|
Route: "/v2/" + repo + "/blobs/uploads/",
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2/" + repo + "/blobs/uploads/" + uuids[0]},
|
|
"Docker-Upload-UUID": {uuids[0]},
|
|
"Range": {"0-0"},
|
|
}),
|
|
},
|
|
})
|
|
offset := 0
|
|
for i, chunk := range chunks {
|
|
uuids = append(uuids, uuid.Generate().String())
|
|
newOffset := offset + len(chunk)
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "PATCH",
|
|
Route: "/v2/" + repo + "/blobs/uploads/" + uuids[i],
|
|
Body: chunk,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2/" + repo + "/blobs/uploads/" + uuids[i+1]},
|
|
"Docker-Upload-UUID": {uuids[i+1]},
|
|
"Range": {fmt.Sprintf("%d-%d", offset, newOffset-1)},
|
|
}),
|
|
},
|
|
})
|
|
offset = newOffset
|
|
}
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "PUT",
|
|
Route: "/v2/" + repo + "/blobs/uploads/" + uuids[len(uuids)-1],
|
|
QueryParams: map[string][]string{
|
|
"digest": {dgst.String()},
|
|
},
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusCreated,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Docker-Content-Digest": {dgst.String()},
|
|
"Content-Range": {fmt.Sprintf("0-%d", offset-1)},
|
|
}),
|
|
},
|
|
})
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "HEAD",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(offset)},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
|
|
upload, err := l.Create(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if upload.ID() != uuids[0] {
|
|
log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0])
|
|
}
|
|
|
|
for _, chunk := range chunks {
|
|
n, err := upload.Write(chunk)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != len(chunk) {
|
|
t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk))
|
|
}
|
|
}
|
|
|
|
blob, err := upload.Commit(ctx, distribution.Descriptor{
|
|
Digest: dgst,
|
|
Size: int64(len(b1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if blob.Size != int64(len(b1)) {
|
|
t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
|
|
}
|
|
}
|
|
|
|
func TestBlobUploadMonolithic(t *testing.T) {
|
|
dgst, b1 := newRandomBlob(1024)
|
|
var m testutil.RequestResponseMap
|
|
repo := "test.example.com/uploadrepo"
|
|
uploadID := uuid.Generate().String()
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "POST",
|
|
Route: "/v2/" + repo + "/blobs/uploads/",
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Location": {"/v2/" + repo + "/blobs/uploads/" + uploadID},
|
|
"Docker-Upload-UUID": {uploadID},
|
|
"Range": {"0-0"},
|
|
}),
|
|
},
|
|
})
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "PATCH",
|
|
Route: "/v2/" + repo + "/blobs/uploads/" + uploadID,
|
|
Body: b1,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Location": {"/v2/" + repo + "/blobs/uploads/" + uploadID},
|
|
"Docker-Upload-UUID": {uploadID},
|
|
"Content-Length": {"0"},
|
|
"Docker-Content-Digest": {dgst.String()},
|
|
"Range": {fmt.Sprintf("0-%d", len(b1)-1)},
|
|
}),
|
|
},
|
|
})
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "PUT",
|
|
Route: "/v2/" + repo + "/blobs/uploads/" + uploadID,
|
|
QueryParams: map[string][]string{
|
|
"digest": {dgst.String()},
|
|
},
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusCreated,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Docker-Content-Digest": {dgst.String()},
|
|
"Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)},
|
|
}),
|
|
},
|
|
})
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "HEAD",
|
|
Route: "/v2/" + repo + "/blobs/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(b1))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
ctx := context.Background()
|
|
r, err := NewRepository(ctx, repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l := r.Blobs(ctx)
|
|
|
|
upload, err := l.Create(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if upload.ID() != uploadID {
|
|
log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID)
|
|
}
|
|
|
|
n, err := upload.ReadFrom(bytes.NewReader(b1))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != int64(len(b1)) {
|
|
t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1))
|
|
}
|
|
|
|
blob, err := upload.Commit(ctx, distribution.Descriptor{
|
|
Digest: dgst,
|
|
Size: int64(len(b1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if blob.Size != int64(len(b1)) {
|
|
t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1))
|
|
}
|
|
}
|
|
|
|
func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
|
|
blobs := make([]schema1.FSLayer, blobCount)
|
|
history := make([]schema1.History, blobCount)
|
|
|
|
for i := 0; i < blobCount; i++ {
|
|
dgst, blob := newRandomBlob((i % 5) * 16)
|
|
|
|
blobs[i] = schema1.FSLayer{BlobSum: dgst}
|
|
history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)}
|
|
}
|
|
|
|
m := schema1.Manifest{
|
|
Name: name,
|
|
Tag: tag,
|
|
Architecture: "x86",
|
|
FSLayers: blobs,
|
|
History: history,
|
|
Versioned: manifest.Versioned{
|
|
SchemaVersion: 1,
|
|
},
|
|
}
|
|
|
|
pk, err := libtrust.GenerateECP256PrivateKey()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
sm, err := schema1.Sign(&m, pk)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
p, err := sm.Payload()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return sm, digest.FromBytes(p), p
|
|
}
|
|
|
|
func addTestManifestWithEtag(repo, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) {
|
|
actualDigest := digest.FromBytes(content)
|
|
getReqWithEtag := testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/manifests/" + reference,
|
|
Headers: http.Header(map[string][]string{
|
|
"If-None-Match": {fmt.Sprintf(`"%s"`, dgst)},
|
|
}),
|
|
}
|
|
|
|
var getRespWithEtag testutil.Response
|
|
if actualDigest.String() == dgst {
|
|
getRespWithEtag = testutil.Response{
|
|
StatusCode: http.StatusNotModified,
|
|
Body: []byte{},
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
}
|
|
} else {
|
|
getRespWithEtag = testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: content,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
}
|
|
|
|
}
|
|
*m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag})
|
|
}
|
|
|
|
func addTestManifest(repo, reference string, content []byte, m *testutil.RequestResponseMap) {
|
|
*m = append(*m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/manifests/" + reference,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: content,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
*m = append(*m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "HEAD",
|
|
Route: "/v2/" + repo + "/manifests/" + reference,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(content))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
}
|
|
|
|
func checkEqualManifest(m1, m2 *schema1.SignedManifest) error {
|
|
if m1.Name != m2.Name {
|
|
return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name)
|
|
}
|
|
if m1.Tag != m2.Tag {
|
|
return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag)
|
|
}
|
|
if len(m1.FSLayers) != len(m2.FSLayers) {
|
|
return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers))
|
|
}
|
|
for i := range m1.FSLayers {
|
|
if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum {
|
|
return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum)
|
|
}
|
|
}
|
|
if len(m1.History) != len(m2.History) {
|
|
return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History))
|
|
}
|
|
for i := range m1.History {
|
|
if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility {
|
|
return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestManifestFetch(t *testing.T) {
|
|
ctx := context.Background()
|
|
repo := "test.example.com/repo"
|
|
m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
|
|
var m testutil.RequestResponseMap
|
|
addTestManifest(repo, dgst.String(), m1.Raw, &m)
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ok, err := ms.Exists(dgst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("Manifest does not exist")
|
|
}
|
|
|
|
manifest, err := ms.Get(dgst)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := checkEqualManifest(manifest, m1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestManifestFetchWithEtag(t *testing.T) {
|
|
repo := "test.example.com/repo/by/tag"
|
|
_, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6)
|
|
var m testutil.RequestResponseMap
|
|
addTestManifestWithEtag(repo, "latest", p1, &m, d1.String())
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = ms.GetByTag("latest", AddEtagToTag("latest", d1.String()))
|
|
if err != distribution.ErrManifestNotModified {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestManifestDelete(t *testing.T) {
|
|
repo := "test.example.com/repo/delete"
|
|
_, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
|
|
_, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
|
|
var m testutil.RequestResponseMap
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "DELETE",
|
|
Route: "/v2/" + repo + "/manifests/" + dgst1.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := ms.Delete(dgst1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ms.Delete(dgst2); err == nil {
|
|
t.Fatal("Expected error deleting unknown manifest")
|
|
}
|
|
// TODO(dmcgowan): Check for specific unknown error
|
|
}
|
|
|
|
func TestManifestPut(t *testing.T) {
|
|
repo := "test.example.com/repo/delete"
|
|
m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6)
|
|
var m testutil.RequestResponseMap
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "PUT",
|
|
Route: "/v2/" + repo + "/manifests/other",
|
|
Body: m1.Raw,
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusAccepted,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {"0"},
|
|
"Docker-Content-Digest": {dgst.String()},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := ms.Put(m1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// TODO(dmcgowan): Check for invalid input error
|
|
}
|
|
|
|
func TestManifestTags(t *testing.T) {
|
|
repo := "test.example.com/repo/tags/list"
|
|
tagsList := []byte(strings.TrimSpace(`
|
|
{
|
|
"name": "test.example.com/repo/tags/list",
|
|
"tags": [
|
|
"tag1",
|
|
"tag2",
|
|
"funtag"
|
|
]
|
|
}
|
|
`))
|
|
var m testutil.RequestResponseMap
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/tags/list",
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: tagsList,
|
|
Headers: http.Header(map[string][]string{
|
|
"Content-Length": {fmt.Sprint(len(tagsList))},
|
|
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
|
|
}),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tags, err := ms.Tags()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(tags) != 3 {
|
|
t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags))
|
|
}
|
|
// TODO(dmcgowan): Check array
|
|
|
|
// TODO(dmcgowan): Check for error cases
|
|
}
|
|
|
|
func TestManifestUnauthorized(t *testing.T) {
|
|
repo := "test.example.com/repo"
|
|
_, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6)
|
|
var m testutil.RequestResponseMap
|
|
|
|
m = append(m, testutil.RequestResponseMapping{
|
|
Request: testutil.Request{
|
|
Method: "GET",
|
|
Route: "/v2/" + repo + "/manifests/" + dgst.String(),
|
|
},
|
|
Response: testutil.Response{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Body: []byte("<html>garbage</html>"),
|
|
},
|
|
})
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
r, err := NewRepository(context.Background(), repo, e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
ms, err := r.Manifests(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = ms.Get(dgst)
|
|
if err == nil {
|
|
t.Fatal("Expected error fetching manifest")
|
|
}
|
|
v2Err, ok := err.(errcode.Error)
|
|
if !ok {
|
|
t.Fatalf("Unexpected error type: %#v", err)
|
|
}
|
|
if v2Err.Code != errcode.ErrorCodeUnauthorized {
|
|
t.Fatalf("Unexpected error code: %s", v2Err.Code.String())
|
|
}
|
|
if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected {
|
|
t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
|
|
}
|
|
}
|
|
|
|
func TestCatalog(t *testing.T) {
|
|
var m testutil.RequestResponseMap
|
|
addTestCatalog(
|
|
"/v2/_catalog?n=5",
|
|
[]byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
entries := make([]string, 5)
|
|
|
|
r, err := NewRegistry(context.Background(), e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
numFilled, err := r.Repositories(ctx, entries, "")
|
|
if err != io.EOF {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if numFilled != 3 {
|
|
t.Fatalf("Got wrong number of repos")
|
|
}
|
|
}
|
|
|
|
func TestCatalogInParts(t *testing.T) {
|
|
var m testutil.RequestResponseMap
|
|
addTestCatalog(
|
|
"/v2/_catalog?n=2",
|
|
[]byte("{\"repositories\":[\"bar\", \"baz\"]}"),
|
|
"</v2/_catalog?last=baz&n=2>", &m)
|
|
addTestCatalog(
|
|
"/v2/_catalog?last=baz&n=2",
|
|
[]byte("{\"repositories\":[\"foo\"]}"),
|
|
"", &m)
|
|
|
|
e, c := testServer(m)
|
|
defer c()
|
|
|
|
entries := make([]string, 2)
|
|
|
|
r, err := NewRegistry(context.Background(), e, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
numFilled, err := r.Repositories(ctx, entries, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if numFilled != 2 {
|
|
t.Fatalf("Got wrong number of repos")
|
|
}
|
|
|
|
numFilled, err = r.Repositories(ctx, entries, "baz")
|
|
if err != io.EOF {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if numFilled != 1 {
|
|
t.Fatalf("Got wrong number of repos")
|
|
}
|
|
}
|
|
|
|
func TestSanitizeLocation(t *testing.T) {
|
|
for _, testcase := range []struct {
|
|
description string
|
|
location string
|
|
source string
|
|
expected string
|
|
err error
|
|
}{
|
|
{
|
|
description: "ensure relative location correctly resolved",
|
|
location: "/v2/foo/baasdf",
|
|
source: "http://blahalaja.com/v1",
|
|
expected: "http://blahalaja.com/v2/foo/baasdf",
|
|
},
|
|
{
|
|
description: "ensure parameters are preserved",
|
|
location: "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
|
|
source: "http://blahalaja.com/v1",
|
|
expected: "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo",
|
|
},
|
|
{
|
|
description: "ensure new hostname overidden",
|
|
location: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
|
|
source: "http://blahalaja.com/v1",
|
|
expected: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf",
|
|
},
|
|
} {
|
|
fatalf := func(format string, args ...interface{}) {
|
|
t.Fatalf(testcase.description+": "+format, args...)
|
|
}
|
|
|
|
s, err := sanitizeLocation(testcase.location, testcase.source)
|
|
if err != testcase.err {
|
|
if testcase.err != nil {
|
|
fatalf("expected error: %v != %v", err, testcase)
|
|
} else {
|
|
fatalf("unexpected error sanitizing: %v", err)
|
|
}
|
|
}
|
|
|
|
if s != testcase.expected {
|
|
fatalf("bad sanitize: %q != %q", s, testcase.expected)
|
|
}
|
|
}
|
|
}
|