forked from TrueCloudLab/distribution
Adds cross-repository blob mounting behavior
Extends blob upload POST endpoint to support mount and from query parameters as described in #634 Signed-off-by: Brian Bland <brian.bland@docker.com>
This commit is contained in:
parent
c426367881
commit
41e30f626b
10 changed files with 416 additions and 10 deletions
|
@ -10,6 +10,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
|
@ -499,6 +500,9 @@ type blobs struct {
|
|||
|
||||
statter distribution.BlobDescriptorService
|
||||
distribution.BlobDeleter
|
||||
|
||||
cacheLock sync.Mutex
|
||||
cachedBlobUpload distribution.BlobWriter
|
||||
}
|
||||
|
||||
func sanitizeLocation(location, base string) (string, error) {
|
||||
|
@ -573,7 +577,20 @@ func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribut
|
|||
}
|
||||
|
||||
func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
||||
bs.cacheLock.Lock()
|
||||
if bs.cachedBlobUpload != nil {
|
||||
upload := bs.cachedBlobUpload
|
||||
bs.cachedBlobUpload = nil
|
||||
bs.cacheLock.Unlock()
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
bs.cacheLock.Unlock()
|
||||
|
||||
u, err := bs.ub.BuildBlobUploadURL(bs.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := bs.client.Post(u, "", nil)
|
||||
if err != nil {
|
||||
|
@ -604,6 +621,45 @@ func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (bs *blobs) Mount(ctx context.Context, sourceRepo string, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
u, err := bs.ub.BuildBlobUploadURL(bs.name, url.Values{"from": {sourceRepo}, "mount": {dgst.String()}})
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
resp, err := bs.client.Post(u, "", nil)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusCreated:
|
||||
return bs.Stat(ctx, dgst)
|
||||
case http.StatusAccepted:
|
||||
// Triggered a blob upload (legacy behavior), so cache the creation info
|
||||
uuid := resp.Header.Get("Docker-Upload-UUID")
|
||||
location, err := sanitizeLocation(resp.Header.Get("Location"), u)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
bs.cacheLock.Lock()
|
||||
bs.cachedBlobUpload = &httpBlobUpload{
|
||||
statter: bs.statter,
|
||||
client: bs.client,
|
||||
uuid: uuid,
|
||||
startedAt: time.Now(),
|
||||
location: location,
|
||||
}
|
||||
bs.cacheLock.Unlock()
|
||||
|
||||
return distribution.Descriptor{}, HandleErrorResponse(resp)
|
||||
default:
|
||||
return distribution.Descriptor{}, HandleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error {
|
||||
return bs.statter.Clear(ctx, dgst)
|
||||
}
|
||||
|
|
|
@ -466,6 +466,61 @@ func TestBlobUploadMonolithic(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBlobMount(t *testing.T) {
|
||||
dgst, content := newRandomBlob(1024)
|
||||
var m testutil.RequestResponseMap
|
||||
repo := "test.example.com/uploadrepo"
|
||||
sourceRepo := "test.example.com/sourcerepo"
|
||||
m = append(m, testutil.RequestResponseMapping{
|
||||
Request: testutil.Request{
|
||||
Method: "POST",
|
||||
Route: "/v2/" + repo + "/blobs/uploads/",
|
||||
QueryParams: map[string][]string{"from": {sourceRepo}, "mount": {dgst.String()}},
|
||||
},
|
||||
Response: testutil.Response{
|
||||
StatusCode: http.StatusCreated,
|
||||
Headers: http.Header(map[string][]string{
|
||||
"Content-Length": {"0"},
|
||||
"Location": {"/v2/" + repo + "/blobs/" + dgst.String()},
|
||||
"Docker-Content-Digest": {dgst.String()},
|
||||
}),
|
||||
},
|
||||
})
|
||||
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)
|
||||
|
||||
stat, err := l.Mount(ctx, sourceRepo, dgst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if stat.Digest != dgst {
|
||||
t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, dgst)
|
||||
}
|
||||
}
|
||||
|
||||
func newRandomSchemaV1Manifest(name, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) {
|
||||
blobs := make([]schema1.FSLayer, blobCount)
|
||||
history := make([]schema1.History, blobCount)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue