441 lines
10 KiB
Go
441 lines
10 KiB
Go
|
package client
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/http/httptest"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
|
||
|
"github.com/docker/distribution/digest"
|
||
|
"github.com/docker/distribution/manifest"
|
||
|
"github.com/docker/distribution/testutil"
|
||
|
)
|
||
|
|
||
|
type testBlob struct {
|
||
|
digest digest.Digest
|
||
|
contents []byte
|
||
|
}
|
||
|
|
||
|
func TestRangeHeaderParser(t *testing.T) {
|
||
|
const (
|
||
|
malformedRangeHeader = "bytes=0-A/C"
|
||
|
emptyRangeHeader = ""
|
||
|
rFirst = 100
|
||
|
rSecond = 200
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
wellformedRangeHeader = fmt.Sprintf("bytes=0-%d/%d", rFirst, rSecond)
|
||
|
)
|
||
|
|
||
|
if _, _, err := parseRangeHeader(malformedRangeHeader); err == nil {
|
||
|
t.Fatalf("malformedRangeHeader: error expected, got nil")
|
||
|
}
|
||
|
|
||
|
if _, _, err := parseRangeHeader(emptyRangeHeader); err == nil {
|
||
|
t.Fatalf("emptyRangeHeader: error expected, got nil")
|
||
|
}
|
||
|
|
||
|
first, second, err := parseRangeHeader(wellformedRangeHeader)
|
||
|
if err != nil {
|
||
|
t.Fatalf("wellformedRangeHeader: unexpected error %v", err)
|
||
|
}
|
||
|
|
||
|
if first != rFirst || second != rSecond {
|
||
|
t.Fatalf("Range has been parsed unproperly: %d/%d", first, second)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func TestPush(t *testing.T) {
|
||
|
name := "hello/world"
|
||
|
tag := "sometag"
|
||
|
testBlobs := []testBlob{
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:12345",
|
||
|
contents: []byte("some contents"),
|
||
|
},
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:98765",
|
||
|
contents: []byte("some other contents"),
|
||
|
},
|
||
|
}
|
||
|
uploadLocations := make([]string, len(testBlobs))
|
||
|
blobs := make([]manifest.FSLayer, len(testBlobs))
|
||
|
history := make([]manifest.History, len(testBlobs))
|
||
|
|
||
|
for i, blob := range testBlobs {
|
||
|
// TODO(bbland): this is returning the same location for all uploads,
|
||
|
// because we can't know which blob will get which location.
|
||
|
// It's sort of okay because we're using unique digests, but this needs
|
||
|
// to change at some point.
|
||
|
uploadLocations[i] = fmt.Sprintf("/v2/%s/blobs/test-uuid", name)
|
||
|
blobs[i] = manifest.FSLayer{BlobSum: blob.digest}
|
||
|
history[i] = manifest.History{V1Compatibility: blob.digest.String()}
|
||
|
}
|
||
|
|
||
|
m := &manifest.SignedManifest{
|
||
|
Manifest: manifest.Manifest{
|
||
|
Name: name,
|
||
|
Tag: tag,
|
||
|
Architecture: "x86",
|
||
|
FSLayers: blobs,
|
||
|
History: history,
|
||
|
Versioned: manifest.Versioned{
|
||
|
SchemaVersion: 1,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
var err error
|
||
|
m.Raw, err = json.Marshal(m)
|
||
|
|
||
|
blobRequestResponseMappings := make([]testutil.RequestResponseMapping, 2*len(testBlobs))
|
||
|
for i, blob := range testBlobs {
|
||
|
blobRequestResponseMappings[2*i] = testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "POST",
|
||
|
Route: "/v2/" + name + "/blobs/uploads/",
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusAccepted,
|
||
|
Headers: http.Header(map[string][]string{
|
||
|
"Location": {uploadLocations[i]},
|
||
|
}),
|
||
|
},
|
||
|
}
|
||
|
blobRequestResponseMappings[2*i+1] = testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "PUT",
|
||
|
Route: uploadLocations[i],
|
||
|
QueryParams: map[string][]string{
|
||
|
"digest": {blob.digest.String()},
|
||
|
},
|
||
|
Body: blob.contents,
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusCreated,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "PUT",
|
||
|
Route: "/v2/" + name + "/manifests/" + tag,
|
||
|
Body: m.Raw,
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
},
|
||
|
}))
|
||
|
var server *httptest.Server
|
||
|
|
||
|
// HACK(stevvooe): Super hack to follow: the request response map approach
|
||
|
// above does not let us correctly format the location header to the
|
||
|
// server url. This handler intercepts and re-writes the location header
|
||
|
// to the server url.
|
||
|
|
||
|
hack := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
w = &headerInterceptingResponseWriter{ResponseWriter: w, serverURL: server.URL}
|
||
|
handler.ServeHTTP(w, r)
|
||
|
})
|
||
|
|
||
|
server = httptest.NewServer(hack)
|
||
|
client, err := New(server.URL)
|
||
|
if err != nil {
|
||
|
t.Fatalf("error creating client: %v", err)
|
||
|
}
|
||
|
objectStore := &memoryObjectStore{
|
||
|
mutex: new(sync.Mutex),
|
||
|
manifestStorage: make(map[string]*manifest.SignedManifest),
|
||
|
layerStorage: make(map[digest.Digest]Layer),
|
||
|
}
|
||
|
|
||
|
for _, blob := range testBlobs {
|
||
|
l, err := objectStore.Layer(blob.digest)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
writer, err := l.Writer()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
writer.SetSize(len(blob.contents))
|
||
|
writer.Write(blob.contents)
|
||
|
writer.Close()
|
||
|
}
|
||
|
|
||
|
objectStore.WriteManifest(name, tag, m)
|
||
|
|
||
|
err = Push(client, objectStore, name, tag)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPull(t *testing.T) {
|
||
|
name := "hello/world"
|
||
|
tag := "sometag"
|
||
|
testBlobs := []testBlob{
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:12345",
|
||
|
contents: []byte("some contents"),
|
||
|
},
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:98765",
|
||
|
contents: []byte("some other contents"),
|
||
|
},
|
||
|
}
|
||
|
blobs := make([]manifest.FSLayer, len(testBlobs))
|
||
|
history := make([]manifest.History, len(testBlobs))
|
||
|
|
||
|
for i, blob := range testBlobs {
|
||
|
blobs[i] = manifest.FSLayer{BlobSum: blob.digest}
|
||
|
history[i] = manifest.History{V1Compatibility: blob.digest.String()}
|
||
|
}
|
||
|
|
||
|
m := &manifest.SignedManifest{
|
||
|
Manifest: manifest.Manifest{
|
||
|
Name: name,
|
||
|
Tag: tag,
|
||
|
Architecture: "x86",
|
||
|
FSLayers: blobs,
|
||
|
History: history,
|
||
|
Versioned: manifest.Versioned{
|
||
|
SchemaVersion: 1,
|
||
|
},
|
||
|
},
|
||
|
}
|
||
|
manifestBytes, err := json.Marshal(m)
|
||
|
|
||
|
blobRequestResponseMappings := make([]testutil.RequestResponseMapping, len(testBlobs))
|
||
|
for i, blob := range testBlobs {
|
||
|
blobRequestResponseMappings[i] = testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "GET",
|
||
|
Route: "/v2/" + name + "/blobs/" + blob.digest.String(),
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
Body: blob.contents,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "GET",
|
||
|
Route: "/v2/" + name + "/manifests/" + tag,
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
Body: manifestBytes,
|
||
|
},
|
||
|
}))
|
||
|
server := httptest.NewServer(handler)
|
||
|
client, err := New(server.URL)
|
||
|
if err != nil {
|
||
|
t.Fatalf("error creating client: %v", err)
|
||
|
}
|
||
|
objectStore := &memoryObjectStore{
|
||
|
mutex: new(sync.Mutex),
|
||
|
manifestStorage: make(map[string]*manifest.SignedManifest),
|
||
|
layerStorage: make(map[digest.Digest]Layer),
|
||
|
}
|
||
|
|
||
|
err = Pull(client, objectStore, name, tag)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
m, err = objectStore.Manifest(name, tag)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
mBytes, err := json.Marshal(m)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
if string(mBytes) != string(manifestBytes) {
|
||
|
t.Fatal("Incorrect manifest")
|
||
|
}
|
||
|
|
||
|
for _, blob := range testBlobs {
|
||
|
l, err := objectStore.Layer(blob.digest)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
reader, err := l.Reader()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
defer reader.Close()
|
||
|
|
||
|
blobBytes, err := ioutil.ReadAll(reader)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
if string(blobBytes) != string(blob.contents) {
|
||
|
t.Fatal("Incorrect blob")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestPullResume(t *testing.T) {
|
||
|
name := "hello/world"
|
||
|
tag := "sometag"
|
||
|
testBlobs := []testBlob{
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:12345",
|
||
|
contents: []byte("some contents"),
|
||
|
},
|
||
|
{
|
||
|
digest: "tarsum.v2+sha256:98765",
|
||
|
contents: []byte("some other contents"),
|
||
|
},
|
||
|
}
|
||
|
layers := make([]manifest.FSLayer, len(testBlobs))
|
||
|
history := make([]manifest.History, len(testBlobs))
|
||
|
|
||
|
for i, layer := range testBlobs {
|
||
|
layers[i] = manifest.FSLayer{BlobSum: layer.digest}
|
||
|
history[i] = manifest.History{V1Compatibility: layer.digest.String()}
|
||
|
}
|
||
|
|
||
|
m := &manifest.Manifest{
|
||
|
Name: name,
|
||
|
Tag: tag,
|
||
|
Architecture: "x86",
|
||
|
FSLayers: layers,
|
||
|
History: history,
|
||
|
Versioned: manifest.Versioned{
|
||
|
SchemaVersion: 1,
|
||
|
},
|
||
|
}
|
||
|
manifestBytes, err := json.Marshal(m)
|
||
|
|
||
|
layerRequestResponseMappings := make([]testutil.RequestResponseMapping, 2*len(testBlobs))
|
||
|
for i, blob := range testBlobs {
|
||
|
layerRequestResponseMappings[2*i] = testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "GET",
|
||
|
Route: "/v2/" + name + "/blobs/" + blob.digest.String(),
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
Body: blob.contents[:len(blob.contents)/2],
|
||
|
Headers: http.Header(map[string][]string{
|
||
|
"Content-Length": {fmt.Sprint(len(blob.contents))},
|
||
|
}),
|
||
|
},
|
||
|
}
|
||
|
layerRequestResponseMappings[2*i+1] = testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "GET",
|
||
|
Route: "/v2/" + name + "/blobs/" + blob.digest.String(),
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
Body: blob.contents[len(blob.contents)/2:],
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i := 0; i < 3; i++ {
|
||
|
layerRequestResponseMappings = append(layerRequestResponseMappings, testutil.RequestResponseMapping{
|
||
|
Request: testutil.Request{
|
||
|
Method: "GET",
|
||
|
Route: "/v2/" + name + "/manifests/" + tag,
|
||
|
},
|
||
|
Response: testutil.Response{
|
||
|
StatusCode: http.StatusOK,
|
||
|
Body: manifestBytes,
|
||
|
},
|
||
|
})
|
||
|
}
|
||
|
|
||
|
handler := testutil.NewHandler(layerRequestResponseMappings)
|
||
|
server := httptest.NewServer(handler)
|
||
|
client, err := New(server.URL)
|
||
|
if err != nil {
|
||
|
t.Fatalf("error creating client: %v", err)
|
||
|
}
|
||
|
objectStore := &memoryObjectStore{
|
||
|
mutex: new(sync.Mutex),
|
||
|
manifestStorage: make(map[string]*manifest.SignedManifest),
|
||
|
layerStorage: make(map[digest.Digest]Layer),
|
||
|
}
|
||
|
|
||
|
for attempts := 0; attempts < 3; attempts++ {
|
||
|
err = Pull(client, objectStore, name, tag)
|
||
|
if err == nil {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
sm, err := objectStore.Manifest(name, tag)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
mBytes, err := json.Marshal(sm)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
if string(mBytes) != string(manifestBytes) {
|
||
|
t.Fatal("Incorrect manifest")
|
||
|
}
|
||
|
|
||
|
for _, blob := range testBlobs {
|
||
|
l, err := objectStore.Layer(blob.digest)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
reader, err := l.Reader()
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
defer reader.Close()
|
||
|
|
||
|
layerBytes, err := ioutil.ReadAll(reader)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
if string(layerBytes) != string(blob.contents) {
|
||
|
t.Fatal("Incorrect blob")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// headerInterceptingResponseWriter is a hacky workaround to re-write the
|
||
|
// location header to have the server url.
|
||
|
type headerInterceptingResponseWriter struct {
|
||
|
http.ResponseWriter
|
||
|
serverURL string
|
||
|
}
|
||
|
|
||
|
func (hirw *headerInterceptingResponseWriter) WriteHeader(status int) {
|
||
|
location := hirw.Header().Get("Location")
|
||
|
if location != "" {
|
||
|
hirw.Header().Set("Location", hirw.serverURL+location)
|
||
|
}
|
||
|
|
||
|
hirw.ResponseWriter.WriteHeader(status)
|
||
|
}
|