Merge pull request #758 from BrianBland/ng-client-routes-update
Updates client to newer routes and changes "layer" to "blob"
This commit is contained in:
commit
0111cf2bd4
8 changed files with 261 additions and 211 deletions
154
client/client.go
154
client/client.go
|
@ -11,62 +11,65 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
// Client implements the client interface to the registry http api
|
||||
type Client interface {
|
||||
// GetImageManifest returns an image manifest for the image at the given
|
||||
// name, tag pair
|
||||
// name, tag pair.
|
||||
GetImageManifest(name, tag string) (*registry.ImageManifest, error)
|
||||
|
||||
// PutImageManifest uploads an image manifest for the image at the given
|
||||
// name, tag pair
|
||||
// name, tag pair.
|
||||
PutImageManifest(name, tag string, imageManifest *registry.ImageManifest) error
|
||||
|
||||
// DeleteImage removes the image at the given name, tag pair
|
||||
// DeleteImage removes the image at the given name, tag pair.
|
||||
DeleteImage(name, tag string) error
|
||||
|
||||
// ListImageTags returns a list of all image tags with the given repository
|
||||
// name
|
||||
// name.
|
||||
ListImageTags(name string) ([]string, error)
|
||||
|
||||
// GetImageLayer returns the image layer at the given name, tarsum pair in
|
||||
// the form of an io.ReadCloser with the length of this layer
|
||||
// A nonzero byteOffset can be provided to receive a partial layer beginning
|
||||
// at the given offset
|
||||
GetImageLayer(name, tarsum string, byteOffset int) (io.ReadCloser, int, error)
|
||||
// BlobLength returns the length of the blob stored at the given name,
|
||||
// digest pair.
|
||||
// Returns a length value of -1 on error or if the blob does not exist.
|
||||
BlobLength(name string, dgst digest.Digest) (int, error)
|
||||
|
||||
// InitiateLayerUpload starts an image upload for the given name, tarsum
|
||||
// pair and returns a unique location url to use for other layer upload
|
||||
// methods
|
||||
// Returns a *registry.LayerAlreadyExistsError if the layer already exists
|
||||
// on the registry
|
||||
InitiateLayerUpload(name, tarsum string) (string, error)
|
||||
// GetBlob returns the blob stored at the given name, digest pair in the
|
||||
// form of an io.ReadCloser with the length of this blob.
|
||||
// A nonzero byteOffset can be provided to receive a partial blob beginning
|
||||
// at the given offset.
|
||||
GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error)
|
||||
|
||||
// GetLayerUploadStatus returns the byte offset and length of the layer at
|
||||
// the given upload location
|
||||
GetLayerUploadStatus(location string) (int, int, error)
|
||||
// InitiateBlobUpload starts a blob upload in the given repository namespace
|
||||
// and returns a unique location url to use for other blob upload methods.
|
||||
InitiateBlobUpload(name string) (string, error)
|
||||
|
||||
// UploadLayer uploads a full image layer to the registry
|
||||
UploadLayer(location string, layer io.ReadCloser, length int, checksum *registry.Checksum) error
|
||||
// GetBlobUploadStatus returns the byte offset and length of the blob at the
|
||||
// given upload location.
|
||||
GetBlobUploadStatus(location string) (int, int, error)
|
||||
|
||||
// UploadLayerChunk uploads a layer chunk with a given length and startByte
|
||||
// to the registry
|
||||
// FinishChunkedLayerUpload must be called to finalize this upload
|
||||
UploadLayerChunk(location string, layerChunk io.ReadCloser, length, startByte int) error
|
||||
// UploadBlob uploads a full blob to the registry.
|
||||
UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error
|
||||
|
||||
// FinishChunkedLayerUpload completes a chunked layer upload at a given
|
||||
// location
|
||||
FinishChunkedLayerUpload(location string, length int, checksum *registry.Checksum) error
|
||||
// UploadBlobChunk uploads a blob chunk with a given length and startByte to
|
||||
// the registry.
|
||||
// FinishChunkedBlobUpload must be called to finalize this upload.
|
||||
UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error
|
||||
|
||||
// CancelLayerUpload deletes all content at the unfinished layer upload
|
||||
// location and invalidates any future calls to this layer upload
|
||||
CancelLayerUpload(location string) error
|
||||
// FinishChunkedBlobUpload completes a chunked blob upload at a given
|
||||
// location.
|
||||
FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error
|
||||
|
||||
// CancelBlobUpload deletes all content at the unfinished blob upload
|
||||
// location and invalidates any future calls to this blob upload.
|
||||
CancelBlobUpload(location string) error
|
||||
}
|
||||
|
||||
// New returns a new Client which operates against a registry with the
|
||||
// given base endpoint
|
||||
// This endpoint should not include /v2/ or any part of the url after this
|
||||
// This endpoint should not include /v2/ or any part of the url after this.
|
||||
func New(endpoint string) Client {
|
||||
return &clientImpl{endpoint}
|
||||
}
|
||||
|
@ -220,9 +223,41 @@ func (r *clientImpl) ListImageTags(name string) ([]string, error) {
|
|||
return tags.Tags, nil
|
||||
}
|
||||
|
||||
func (r *clientImpl) GetImageLayer(name, tarsum string, byteOffset int) (io.ReadCloser, int, error) {
|
||||
func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) {
|
||||
response, err := http.Head(fmt.Sprintf("%s/v2/%s/blob/%s", r.Endpoint, name, dgst))
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// TODO(bbland): handle other status codes, like 5xx errors
|
||||
switch {
|
||||
case response.StatusCode == http.StatusOK:
|
||||
lengthHeader := response.Header.Get("Content-Length")
|
||||
length, err := strconv.ParseInt(lengthHeader, 10, 0)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return int(length), nil
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return -1, nil
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
err = decoder.Decode(&errors)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return -1, errors
|
||||
default:
|
||||
response.Body.Close()
|
||||
return -1, ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error) {
|
||||
getRequest, err := http.NewRequest("GET",
|
||||
fmt.Sprintf("%s/v2/%s/layer/%s", r.Endpoint, name, tarsum), nil)
|
||||
fmt.Sprintf("%s/v2/%s/blob/%s", r.Endpoint, name, dgst), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -233,9 +268,6 @@ func (r *clientImpl) GetImageLayer(name, tarsum string, byteOffset int) (io.Read
|
|||
return nil, 0, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
return nil, 0, ®istry.LayerNotFoundError{Name: name, TarSum: tarsum}
|
||||
}
|
||||
// TODO(bbland): handle other status codes, like 5xx errors
|
||||
switch {
|
||||
case response.StatusCode == http.StatusOK:
|
||||
|
@ -247,7 +279,7 @@ func (r *clientImpl) GetImageLayer(name, tarsum string, byteOffset int) (io.Read
|
|||
return response.Body, int(length), nil
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
response.Body.Close()
|
||||
return nil, 0, ®istry.LayerNotFoundError{Name: name, TarSum: tarsum}
|
||||
return nil, 0, ®istry.BlobNotFoundError{Name: name, Digest: dgst}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -262,9 +294,9 @@ func (r *clientImpl) GetImageLayer(name, tarsum string, byteOffset int) (io.Read
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) InitiateLayerUpload(name, tarsum string) (string, error) {
|
||||
func (r *clientImpl) InitiateBlobUpload(name string) (string, error) {
|
||||
postRequest, err := http.NewRequest("POST",
|
||||
fmt.Sprintf("%s/v2/%s/layer/%s/upload/", r.Endpoint, name, tarsum), nil)
|
||||
fmt.Sprintf("%s/v2/%s/blob/upload/", r.Endpoint, name), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -279,8 +311,8 @@ func (r *clientImpl) InitiateLayerUpload(name, tarsum string) (string, error) {
|
|||
switch {
|
||||
case response.StatusCode == http.StatusAccepted:
|
||||
return response.Header.Get("Location"), nil
|
||||
case response.StatusCode == http.StatusNotModified:
|
||||
return "", ®istry.LayerAlreadyExistsError{Name: name, TarSum: tarsum}
|
||||
// case response.StatusCode == http.StatusNotFound:
|
||||
// return
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -294,7 +326,7 @@ func (r *clientImpl) InitiateLayerUpload(name, tarsum string) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) GetLayerUploadStatus(location string) (int, int, error) {
|
||||
func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) {
|
||||
response, err := http.Get(fmt.Sprintf("%s%s", r.Endpoint, location))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
@ -306,7 +338,7 @@ func (r *clientImpl) GetLayerUploadStatus(location string) (int, int, error) {
|
|||
case response.StatusCode == http.StatusNoContent:
|
||||
return parseRangeHeader(response.Header.Get("Range"))
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return 0, 0, ®istry.LayerUploadNotFoundError{Location: location}
|
||||
return 0, 0, ®istry.BlobUploadNotFoundError{Location: location}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -320,18 +352,18 @@ func (r *clientImpl) GetLayerUploadStatus(location string) (int, int, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) UploadLayer(location string, layer io.ReadCloser, length int, checksum *registry.Checksum) error {
|
||||
defer layer.Close()
|
||||
func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error {
|
||||
defer blob.Close()
|
||||
|
||||
putRequest, err := http.NewRequest("PUT",
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), layer)
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
queryValues := url.Values{}
|
||||
queryValues.Set("length", fmt.Sprint(length))
|
||||
queryValues.Set(checksum.HashAlgorithm, checksum.Sum)
|
||||
queryValues.Set("digest", dgst.String())
|
||||
putRequest.URL.RawQuery = queryValues.Encode()
|
||||
|
||||
putRequest.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
@ -348,7 +380,7 @@ func (r *clientImpl) UploadLayer(location string, layer io.ReadCloser, length in
|
|||
case response.StatusCode == http.StatusCreated:
|
||||
return nil
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return ®istry.LayerUploadNotFoundError{Location: location}
|
||||
return ®istry.BlobUploadNotFoundError{Location: location}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -362,11 +394,11 @@ func (r *clientImpl) UploadLayer(location string, layer io.ReadCloser, length in
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) UploadLayerChunk(location string, layerChunk io.ReadCloser, length, startByte int) error {
|
||||
defer layerChunk.Close()
|
||||
func (r *clientImpl) UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error {
|
||||
defer blobChunk.Close()
|
||||
|
||||
putRequest, err := http.NewRequest("PUT",
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), layerChunk)
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), blobChunk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -389,17 +421,17 @@ func (r *clientImpl) UploadLayerChunk(location string, layerChunk io.ReadCloser,
|
|||
case response.StatusCode == http.StatusAccepted:
|
||||
return nil
|
||||
case response.StatusCode == http.StatusRequestedRangeNotSatisfiable:
|
||||
lastValidRange, layerSize, err := parseRangeHeader(response.Header.Get("Range"))
|
||||
lastValidRange, blobSize, err := parseRangeHeader(response.Header.Get("Range"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ®istry.LayerUploadInvalidRangeError{
|
||||
return ®istry.BlobUploadInvalidRangeError{
|
||||
Location: location,
|
||||
LastValidRange: lastValidRange,
|
||||
LayerSize: layerSize,
|
||||
BlobSize: blobSize,
|
||||
}
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return ®istry.LayerUploadNotFoundError{Location: location}
|
||||
return ®istry.BlobUploadNotFoundError{Location: location}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -413,7 +445,7 @@ func (r *clientImpl) UploadLayerChunk(location string, layerChunk io.ReadCloser,
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) FinishChunkedLayerUpload(location string, length int, checksum *registry.Checksum) error {
|
||||
func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error {
|
||||
putRequest, err := http.NewRequest("PUT",
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), nil)
|
||||
if err != nil {
|
||||
|
@ -422,7 +454,7 @@ func (r *clientImpl) FinishChunkedLayerUpload(location string, length int, check
|
|||
|
||||
queryValues := new(url.Values)
|
||||
queryValues.Set("length", fmt.Sprint(length))
|
||||
queryValues.Set(checksum.HashAlgorithm, checksum.Sum)
|
||||
queryValues.Set("digest", dgst.String())
|
||||
putRequest.URL.RawQuery = queryValues.Encode()
|
||||
|
||||
putRequest.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
@ -441,7 +473,7 @@ func (r *clientImpl) FinishChunkedLayerUpload(location string, length int, check
|
|||
case response.StatusCode == http.StatusCreated:
|
||||
return nil
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return ®istry.LayerUploadNotFoundError{Location: location}
|
||||
return ®istry.BlobUploadNotFoundError{Location: location}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -455,7 +487,7 @@ func (r *clientImpl) FinishChunkedLayerUpload(location string, length int, check
|
|||
}
|
||||
}
|
||||
|
||||
func (r *clientImpl) CancelLayerUpload(location string) error {
|
||||
func (r *clientImpl) CancelBlobUpload(location string) error {
|
||||
deleteRequest, err := http.NewRequest("DELETE",
|
||||
fmt.Sprintf("%s%s", r.Endpoint, location), nil)
|
||||
if err != nil {
|
||||
|
@ -473,7 +505,7 @@ func (r *clientImpl) CancelLayerUpload(location string) error {
|
|||
case response.StatusCode == http.StatusNoContent:
|
||||
return nil
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return ®istry.LayerUploadNotFoundError{Location: location}
|
||||
return ®istry.BlobUploadNotFoundError{Location: location}
|
||||
case response.StatusCode >= 400 && response.StatusCode < 500:
|
||||
errors := new(registry.Errors)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
|
@ -490,7 +522,7 @@ func (r *clientImpl) CancelLayerUpload(location string) error {
|
|||
// imageManifestURL is a helper method for returning the full url to an image
|
||||
// manifest
|
||||
func (r *clientImpl) imageManifestURL(name, tag string) string {
|
||||
return fmt.Sprintf("%s/v2/%s/image/%s", r.Endpoint, name, tag)
|
||||
return fmt.Sprintf("%s/v2/%s/manifest/%s", r.Endpoint, name, tag)
|
||||
}
|
||||
|
||||
// parseRangeHeader parses out the offset and length from a returned Range
|
||||
|
|
|
@ -10,101 +10,104 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/test"
|
||||
"github.com/docker/docker-registry/common/testutil"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
type testLayer struct {
|
||||
tarSum string
|
||||
type testBlob struct {
|
||||
digest digest.Digest
|
||||
contents []byte
|
||||
}
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
name := "hello/world"
|
||||
tag := "sometag"
|
||||
testLayers := []testLayer{
|
||||
testBlobs := []testBlob{
|
||||
{
|
||||
tarSum: "12345",
|
||||
digest: "12345",
|
||||
contents: []byte("some contents"),
|
||||
},
|
||||
{
|
||||
tarSum: "98765",
|
||||
digest: "98765",
|
||||
contents: []byte("some other contents"),
|
||||
},
|
||||
}
|
||||
uploadLocations := make([]string, len(testLayers))
|
||||
layers := make([]registry.FSLayer, len(testLayers))
|
||||
history := make([]registry.ManifestHistory, len(testLayers))
|
||||
uploadLocations := make([]string, len(testBlobs))
|
||||
blobs := make([]registry.FSLayer, len(testBlobs))
|
||||
history := make([]registry.ManifestHistory, len(testBlobs))
|
||||
|
||||
for i, layer := range testLayers {
|
||||
uploadLocations[i] = fmt.Sprintf("/v2/%s/layer/%s/upload-location-%d", name, layer.tarSum, i)
|
||||
layers[i] = registry.FSLayer{BlobSum: layer.tarSum}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: layer.tarSum}
|
||||
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/blob/test-uuid", name)
|
||||
blobs[i] = registry.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
}
|
||||
|
||||
manifest := ®istry.ImageManifest{
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Architecture: "x86",
|
||||
FSLayers: layers,
|
||||
FSLayers: blobs,
|
||||
History: history,
|
||||
SchemaVersion: 1,
|
||||
}
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
|
||||
layerRequestResponseMappings := make([]test.RequestResponseMapping, 2*len(testLayers))
|
||||
for i, layer := range testLayers {
|
||||
layerRequestResponseMappings[2*i] = test.RequestResponseMapping{
|
||||
Request: test.Request{
|
||||
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 + "/layer/" + layer.tarSum + "/upload/",
|
||||
Route: "/v2/" + name + "/blob/upload/",
|
||||
},
|
||||
Responses: []test.Response{
|
||||
{
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusAccepted,
|
||||
Headers: http.Header(map[string][]string{
|
||||
"Location": {uploadLocations[i]},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
layerRequestResponseMappings[2*i+1] = test.RequestResponseMapping{
|
||||
Request: test.Request{
|
||||
blobRequestResponseMappings[2*i+1] = testutil.RequestResponseMapping{
|
||||
Request: testutil.Request{
|
||||
Method: "PUT",
|
||||
Route: uploadLocations[i],
|
||||
Body: layer.contents,
|
||||
QueryParams: map[string][]string{
|
||||
"length": {fmt.Sprint(len(blob.contents))},
|
||||
"digest": {blob.digest.String()},
|
||||
},
|
||||
Responses: []test.Response{
|
||||
{
|
||||
Body: blob.contents,
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusCreated,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handler := test.NewHandler(append(layerRequestResponseMappings, test.RequestResponseMap{
|
||||
test.RequestResponseMapping{
|
||||
Request: test.Request{
|
||||
handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMap{
|
||||
testutil.RequestResponseMapping{
|
||||
Request: testutil.Request{
|
||||
Method: "PUT",
|
||||
Route: "/v2/" + name + "/image/" + tag,
|
||||
Route: "/v2/" + name + "/manifest/" + tag,
|
||||
Body: manifestBytes,
|
||||
},
|
||||
Responses: []test.Response{
|
||||
{
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
},
|
||||
},
|
||||
},
|
||||
}...))
|
||||
server := httptest.NewServer(handler)
|
||||
client := New(server.URL)
|
||||
objectStore := &memoryObjectStore{
|
||||
mutex: new(sync.Mutex),
|
||||
manifestStorage: make(map[string]*registry.ImageManifest),
|
||||
layerStorage: make(map[string]Layer),
|
||||
layerStorage: make(map[digest.Digest]Layer),
|
||||
}
|
||||
|
||||
for _, layer := range testLayers {
|
||||
l, err := objectStore.Layer(layer.tarSum)
|
||||
for _, blob := range testBlobs {
|
||||
l, err := objectStore.Layer(blob.digest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -114,7 +117,7 @@ func TestPush(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writer.Write(layer.contents)
|
||||
writer.Write(blob.contents)
|
||||
writer.Close()
|
||||
}
|
||||
|
||||
|
@ -129,70 +132,66 @@ func TestPush(t *testing.T) {
|
|||
func TestPull(t *testing.T) {
|
||||
name := "hello/world"
|
||||
tag := "sometag"
|
||||
testLayers := []testLayer{
|
||||
testBlobs := []testBlob{
|
||||
{
|
||||
tarSum: "12345",
|
||||
digest: "12345",
|
||||
contents: []byte("some contents"),
|
||||
},
|
||||
{
|
||||
tarSum: "98765",
|
||||
digest: "98765",
|
||||
contents: []byte("some other contents"),
|
||||
},
|
||||
}
|
||||
layers := make([]registry.FSLayer, len(testLayers))
|
||||
history := make([]registry.ManifestHistory, len(testLayers))
|
||||
blobs := make([]registry.FSLayer, len(testBlobs))
|
||||
history := make([]registry.ManifestHistory, len(testBlobs))
|
||||
|
||||
for i, layer := range testLayers {
|
||||
layers[i] = registry.FSLayer{BlobSum: layer.tarSum}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: layer.tarSum}
|
||||
for i, blob := range testBlobs {
|
||||
blobs[i] = registry.FSLayer{BlobSum: blob.digest}
|
||||
history[i] = registry.ManifestHistory{V1Compatibility: blob.digest.String()}
|
||||
}
|
||||
|
||||
manifest := ®istry.ImageManifest{
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Architecture: "x86",
|
||||
FSLayers: layers,
|
||||
FSLayers: blobs,
|
||||
History: history,
|
||||
SchemaVersion: 1,
|
||||
}
|
||||
manifestBytes, err := json.Marshal(manifest)
|
||||
|
||||
layerRequestResponseMappings := make([]test.RequestResponseMapping, len(testLayers))
|
||||
for i, layer := range testLayers {
|
||||
layerRequestResponseMappings[i] = test.RequestResponseMapping{
|
||||
Request: test.Request{
|
||||
blobRequestResponseMappings := make([]testutil.RequestResponseMapping, len(testBlobs))
|
||||
for i, blob := range testBlobs {
|
||||
blobRequestResponseMappings[i] = testutil.RequestResponseMapping{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/" + name + "/layer/" + layer.tarSum,
|
||||
Route: "/v2/" + name + "/blob/" + blob.digest.String(),
|
||||
},
|
||||
Responses: []test.Response{
|
||||
{
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: layer.contents,
|
||||
},
|
||||
Body: blob.contents,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handler := test.NewHandler(append(layerRequestResponseMappings, test.RequestResponseMap{
|
||||
test.RequestResponseMapping{
|
||||
Request: test.Request{
|
||||
handler := testutil.NewHandler(append(blobRequestResponseMappings, testutil.RequestResponseMap{
|
||||
testutil.RequestResponseMapping{
|
||||
Request: testutil.Request{
|
||||
Method: "GET",
|
||||
Route: "/v2/" + name + "/image/" + tag,
|
||||
Route: "/v2/" + name + "/manifest/" + tag,
|
||||
},
|
||||
Responses: []test.Response{
|
||||
{
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: manifestBytes,
|
||||
},
|
||||
},
|
||||
},
|
||||
}...))
|
||||
server := httptest.NewServer(handler)
|
||||
client := New(server.URL)
|
||||
objectStore := &memoryObjectStore{
|
||||
mutex: new(sync.Mutex),
|
||||
manifestStorage: make(map[string]*registry.ImageManifest),
|
||||
layerStorage: make(map[string]Layer),
|
||||
layerStorage: make(map[digest.Digest]Layer),
|
||||
}
|
||||
|
||||
err = Pull(client, objectStore, name, tag)
|
||||
|
@ -214,8 +213,8 @@ func TestPull(t *testing.T) {
|
|||
t.Fatal("Incorrect manifest")
|
||||
}
|
||||
|
||||
for _, layer := range testLayers {
|
||||
l, err := objectStore.Layer(layer.tarSum)
|
||||
for _, blob := range testBlobs {
|
||||
l, err := objectStore.Layer(blob.digest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -226,13 +225,13 @@ func TestPull(t *testing.T) {
|
|||
}
|
||||
defer reader.Close()
|
||||
|
||||
layerBytes, err := ioutil.ReadAll(reader)
|
||||
blobBytes, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if string(layerBytes) != string(layer.contents) {
|
||||
t.Fatal("Incorrect layer")
|
||||
if string(blobBytes) != string(blob.contents) {
|
||||
t.Fatal("Incorrect blob")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/docker/docker-registry"
|
||||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -34,7 +35,7 @@ type ObjectStore interface {
|
|||
WriteManifest(name, tag string, manifest *registry.ImageManifest) error
|
||||
|
||||
// Layer returns a handle to a layer for reading and writing
|
||||
Layer(blobSum string) (Layer, error)
|
||||
Layer(dgst digest.Digest) (Layer, error)
|
||||
}
|
||||
|
||||
// Layer is a generic image layer interface.
|
||||
|
@ -56,7 +57,7 @@ type Layer interface {
|
|||
type memoryObjectStore struct {
|
||||
mutex *sync.Mutex
|
||||
manifestStorage map[string]*registry.ImageManifest
|
||||
layerStorage map[string]Layer
|
||||
layerStorage map[digest.Digest]Layer
|
||||
}
|
||||
|
||||
func (objStore *memoryObjectStore) Manifest(name, tag string) (*registry.ImageManifest, error) {
|
||||
|
@ -78,14 +79,14 @@ func (objStore *memoryObjectStore) WriteManifest(name, tag string, manifest *reg
|
|||
return nil
|
||||
}
|
||||
|
||||
func (objStore *memoryObjectStore) Layer(blobSum string) (Layer, error) {
|
||||
func (objStore *memoryObjectStore) Layer(dgst digest.Digest) (Layer, error) {
|
||||
objStore.mutex.Lock()
|
||||
defer objStore.mutex.Unlock()
|
||||
|
||||
layer, ok := objStore.layerStorage[blobSum]
|
||||
layer, ok := objStore.layerStorage[dgst]
|
||||
if !ok {
|
||||
layer = &memoryLayer{cond: sync.NewCond(new(sync.Mutex))}
|
||||
objStore.layerStorage[blobSum] = layer
|
||||
objStore.layerStorage[dgst] = layer
|
||||
}
|
||||
|
||||
return layer, nil
|
||||
|
|
|
@ -99,7 +99,7 @@ func pullLayer(c Client, objectStore ObjectStore, name string, fsLayer registry.
|
|||
}
|
||||
defer writer.Close()
|
||||
|
||||
layerReader, length, err := c.GetImageLayer(name, fsLayer.BlobSum, 0)
|
||||
layerReader, length, err := c.GetBlob(name, fsLayer.BlobSum, 0)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
|
|
|
@ -2,7 +2,6 @@ package client
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
|
@ -89,25 +88,10 @@ func pushLayer(c Client, objectStore ObjectStore, name string, fsLayer registry.
|
|||
}).Warn("Unable to read local layer")
|
||||
return err
|
||||
}
|
||||
|
||||
location, err := c.InitiateLayerUpload(name, fsLayer.BlobSum)
|
||||
if _, ok := err.(*registry.LayerAlreadyExistsError); ok {
|
||||
log.WithField("layer", fsLayer).Info("Layer already exists")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"layer": fsLayer,
|
||||
}).Warn("Unable to upload layer")
|
||||
return err
|
||||
}
|
||||
defer layerReader.Close()
|
||||
|
||||
layerBuffer := new(bytes.Buffer)
|
||||
checksum := sha1.New()
|
||||
teeReader := io.TeeReader(layerReader, checksum)
|
||||
|
||||
_, err = io.Copy(layerBuffer, teeReader)
|
||||
layerSize, err := io.Copy(layerBuffer, layerReader)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
|
@ -116,9 +100,29 @@ func pushLayer(c Client, objectStore ObjectStore, name string, fsLayer registry.
|
|||
return err
|
||||
}
|
||||
|
||||
err = c.UploadLayer(location, ioutil.NopCloser(layerBuffer), layerBuffer.Len(),
|
||||
®istry.Checksum{HashAlgorithm: "sha1", Sum: string(checksum.Sum(nil))},
|
||||
)
|
||||
length, err := c.BlobLength(name, fsLayer.BlobSum)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"layer": fsLayer,
|
||||
}).Warn("Unable to check existence of remote layer")
|
||||
return err
|
||||
}
|
||||
if length >= 0 {
|
||||
log.WithField("layer", fsLayer).Info("Layer already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
location, err := c.InitiateBlobUpload(name)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
"layer": fsLayer,
|
||||
}).Warn("Unable to upload layer")
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.UploadBlob(location, ioutil.NopCloser(layerBuffer), int(layerSize), fsLayer.BlobSum)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"error": err,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package test
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -6,16 +6,18 @@ import (
|
|||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RequestResponseMap is a mapping from Requests to Responses
|
||||
// RequestResponseMap is an ordered mapping from Requests to Responses
|
||||
type RequestResponseMap []RequestResponseMapping
|
||||
|
||||
// RequestResponseMapping defines an ordered list of Responses to be sent in
|
||||
// response to a given Request
|
||||
// RequestResponseMapping defines a Response to be sent in response to a given
|
||||
// Request
|
||||
type RequestResponseMapping struct {
|
||||
Request Request
|
||||
Responses []Response
|
||||
Response Response
|
||||
}
|
||||
|
||||
// TODO(bbland): add support for request headers
|
||||
|
@ -28,12 +30,28 @@ type Request struct {
|
|||
// Route is the http route of this request
|
||||
Route string
|
||||
|
||||
// QueryParams are the query parameters of this request
|
||||
QueryParams map[string][]string
|
||||
|
||||
// Body is the byte contents of the http request
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (r Request) String() string {
|
||||
return fmt.Sprintf("%s %s\n%s", r.Method, r.Route, r.Body)
|
||||
queryString := ""
|
||||
if len(r.QueryParams) > 0 {
|
||||
queryString = "?"
|
||||
keys := make([]string, 0, len(r.QueryParams))
|
||||
for k := range r.QueryParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
queryString += strings.Join(r.QueryParams[k], "&") + "&"
|
||||
}
|
||||
queryString = queryString[:len(queryString)-1]
|
||||
}
|
||||
return fmt.Sprintf("%s %s%s\n%s", r.Method, r.Route, queryString, r.Body)
|
||||
}
|
||||
|
||||
// Response is a simplified http.Response object
|
||||
|
@ -61,7 +79,12 @@ type testHandler struct {
|
|||
func NewHandler(requestResponseMap RequestResponseMap) http.Handler {
|
||||
responseMap := make(map[string][]Response)
|
||||
for _, mapping := range requestResponseMap {
|
||||
responseMap[mapping.Request.String()] = mapping.Responses
|
||||
responses, ok := responseMap[mapping.Request.String()]
|
||||
if ok {
|
||||
responseMap[mapping.Request.String()] = append(responses, mapping.Response)
|
||||
} else {
|
||||
responseMap[mapping.Request.String()] = []Response{mapping.Response}
|
||||
}
|
||||
}
|
||||
return &testHandler{responseMap: responseMap}
|
||||
}
|
||||
|
@ -73,6 +96,7 @@ func (app *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
request := Request{
|
||||
Method: r.Method,
|
||||
Route: r.URL.Path,
|
||||
QueryParams: r.URL.Query(),
|
||||
Body: requestBody,
|
||||
}
|
||||
|
53
errors.go
53
errors.go
|
@ -3,6 +3,8 @@ package registry
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
)
|
||||
|
||||
// ErrorCode represents the error type. The errors are serialized via strings
|
||||
|
@ -224,57 +226,44 @@ func (e *ImageManifestNotFoundError) Error() string {
|
|||
e.Name, e.Tag)
|
||||
}
|
||||
|
||||
// LayerAlreadyExistsError is returned when attempting to create a new layer
|
||||
// that already exists in the registry.
|
||||
type LayerAlreadyExistsError struct {
|
||||
Name string
|
||||
TarSum string
|
||||
}
|
||||
|
||||
func (e *LayerAlreadyExistsError) Error() string {
|
||||
return fmt.Sprintf("Layer already found with Name: %s, TarSum: %s",
|
||||
e.Name, e.TarSum)
|
||||
}
|
||||
|
||||
// LayerNotFoundError is returned when making an operation against a given image
|
||||
// BlobNotFoundError is returned when making an operation against a given image
|
||||
// layer that does not exist in the registry.
|
||||
type LayerNotFoundError struct {
|
||||
type BlobNotFoundError struct {
|
||||
Name string
|
||||
TarSum string
|
||||
Digest digest.Digest
|
||||
}
|
||||
|
||||
func (e *LayerNotFoundError) Error() string {
|
||||
return fmt.Sprintf("No layer found with Name: %s, TarSum: %s",
|
||||
e.Name, e.TarSum)
|
||||
func (e *BlobNotFoundError) Error() string {
|
||||
return fmt.Sprintf("No blob found with Name: %s, Digest: %s",
|
||||
e.Name, e.Digest)
|
||||
}
|
||||
|
||||
// LayerUploadNotFoundError is returned when making a layer upload operation
|
||||
// against an invalid layer upload location url
|
||||
// BlobUploadNotFoundError is returned when making a blob upload operation against an
|
||||
// invalid blob upload location url.
|
||||
// This may be the result of using a cancelled, completed, or stale upload
|
||||
// location.
|
||||
type LayerUploadNotFoundError struct {
|
||||
type BlobUploadNotFoundError struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
func (e *LayerUploadNotFoundError) Error() string {
|
||||
return fmt.Sprintf("No layer found upload found at Location: %s",
|
||||
e.Location)
|
||||
func (e *BlobUploadNotFoundError) Error() string {
|
||||
return fmt.Sprintf("No blob upload found at Location: %s", e.Location)
|
||||
}
|
||||
|
||||
// LayerUploadInvalidRangeError is returned when attempting to upload an image
|
||||
// layer chunk that is out of order.
|
||||
// This provides the known LayerSize and LastValidRange which can be used to
|
||||
// BlobUploadInvalidRangeError is returned when attempting to upload an image
|
||||
// blob chunk that is out of order.
|
||||
// This provides the known BlobSize and LastValidRange which can be used to
|
||||
// resume the upload.
|
||||
type LayerUploadInvalidRangeError struct {
|
||||
type BlobUploadInvalidRangeError struct {
|
||||
Location string
|
||||
LastValidRange int
|
||||
LayerSize int
|
||||
BlobSize int
|
||||
}
|
||||
|
||||
func (e *LayerUploadInvalidRangeError) Error() string {
|
||||
func (e *BlobUploadInvalidRangeError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"Invalid range provided for upload at Location: %s. Last Valid Range: %d, Layer Size: %d",
|
||||
e.Location, e.LastValidRange, e.LayerSize)
|
||||
"Invalid range provided for upload at Location: %s. Last Valid Range: %d, Blob Size: %d",
|
||||
e.Location, e.LastValidRange, e.BlobSize)
|
||||
}
|
||||
|
||||
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker-registry/digest"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
|
@ -52,7 +53,7 @@ func (m *ImageManifest) UnmarshalJSON(b []byte) error {
|
|||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum string `json:"blobSum"`
|
||||
BlobSum digest.Digest `json:"blobSum"`
|
||||
}
|
||||
|
||||
// ManifestHistory stores unstructured v1 compatibility information
|
||||
|
|
Loading…
Reference in a new issue