Merge pull request #519 from stevvooe/blob-service-refactor

Refactor Blob Service API
This commit is contained in:
Stephen Day 2015-05-15 17:52:15 -07:00
commit 24f607cdf4
44 changed files with 2426 additions and 2270 deletions

View file

@ -93,8 +93,8 @@ func TestURLPrefix(t *testing.T) {
} }
// TestLayerAPI conducts a full test of the of the layer api. // TestBlobAPI conducts a full test of the of the blob api.
func TestLayerAPI(t *testing.T) { func TestBlobAPI(t *testing.T) {
// TODO(stevvooe): This test code is complete junk but it should cover the // TODO(stevvooe): This test code is complete junk but it should cover the
// complete flow. This must be broken down and checked against the // complete flow. This must be broken down and checked against the
// specification *before* we submit the final to docker core. // specification *before* we submit the final to docker core.
@ -213,6 +213,13 @@ func TestLayerAPI(t *testing.T) {
// Now, push just a chunk // Now, push just a chunk
layerFile.Seek(0, 0) layerFile.Seek(0, 0)
canonicalDigester := digest.NewCanonicalDigester()
if _, err := io.Copy(canonicalDigester, layerFile); err != nil {
t.Fatalf("error copying to digest: %v", err)
}
canonicalDigest := canonicalDigester.Digest()
layerFile.Seek(0, 0)
uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName) uploadURLBase, uploadUUID = startPushLayer(t, env.builder, imageName)
uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength) uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength)
finishUpload(t, env.builder, imageName, uploadURLBase, dgst) finishUpload(t, env.builder, imageName, uploadURLBase, dgst)
@ -226,7 +233,7 @@ func TestLayerAPI(t *testing.T) {
checkResponse(t, "checking head on existing layer", resp, http.StatusOK) checkResponse(t, "checking head on existing layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)}, "Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{layerDigest.String()}, "Docker-Content-Digest": []string{canonicalDigest.String()},
}) })
// ---------------- // ----------------
@ -239,7 +246,7 @@ func TestLayerAPI(t *testing.T) {
checkResponse(t, "fetching layer", resp, http.StatusOK) checkResponse(t, "fetching layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)}, "Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{layerDigest.String()}, "Docker-Content-Digest": []string{canonicalDigest.String()},
}) })
// Verify the body // Verify the body
@ -272,9 +279,9 @@ func TestLayerAPI(t *testing.T) {
checkResponse(t, "fetching layer", resp, http.StatusOK) checkResponse(t, "fetching layer", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{ checkHeaders(t, resp, http.Header{
"Content-Length": []string{fmt.Sprint(layerLength)}, "Content-Length": []string{fmt.Sprint(layerLength)},
"Docker-Content-Digest": []string{layerDigest.String()}, "Docker-Content-Digest": []string{canonicalDigest.String()},
"ETag": []string{layerDigest.String()}, "ETag": []string{canonicalDigest.String()},
"Cache-Control": []string{"max-age=86400"}, "Cache-Control": []string{"max-age=31536000"},
}) })
// Matching etag, gives 304 // Matching etag, gives 304

View file

@ -67,9 +67,9 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
}) })
app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameManifest, imageManifestDispatcher)
app.register(v2.RouteNameTags, tagsDispatcher) app.register(v2.RouteNameTags, tagsDispatcher)
app.register(v2.RouteNameBlob, layerDispatcher) app.register(v2.RouteNameBlob, blobDispatcher)
app.register(v2.RouteNameBlobUpload, layerUploadDispatcher) app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
app.register(v2.RouteNameBlobUploadChunk, layerUploadDispatcher) app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)
var err error var err error
app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters()) app.driver, err = factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters())
@ -103,18 +103,24 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
// configure storage caches // configure storage caches
if cc, ok := configuration.Storage["cache"]; ok { if cc, ok := configuration.Storage["cache"]; ok {
switch cc["layerinfo"] { v, ok := cc["blobdescriptor"]
if !ok {
// Backwards compatible: "layerinfo" == "blobdescriptor"
v = cc["layerinfo"]
}
switch v {
case "redis": case "redis":
if app.redis == nil { if app.redis == nil {
panic("redis configuration required to use for layerinfo cache") panic("redis configuration required to use for layerinfo cache")
} }
app.registry = storage.NewRegistryWithDriver(app, app.driver, cache.NewRedisLayerInfoCache(app.redis)) app.registry = storage.NewRegistryWithDriver(app, app.driver, cache.NewRedisBlobDescriptorCacheProvider(app.redis))
ctxu.GetLogger(app).Infof("using redis layerinfo cache") ctxu.GetLogger(app).Infof("using redis blob descriptor cache")
case "inmemory": case "inmemory":
app.registry = storage.NewRegistryWithDriver(app, app.driver, cache.NewInMemoryLayerInfoCache()) app.registry = storage.NewRegistryWithDriver(app, app.driver, cache.NewInMemoryBlobDescriptorCacheProvider())
ctxu.GetLogger(app).Infof("using inmemory layerinfo cache") ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache")
default: default:
if cc["layerinfo"] != "" { if v != "" {
ctxu.GetLogger(app).Warnf("unkown cache type %q, caching disabled", configuration.Storage["cache"]) ctxu.GetLogger(app).Warnf("unkown cache type %q, caching disabled", configuration.Storage["cache"])
} }
} }

View file

@ -30,7 +30,7 @@ func TestAppDispatcher(t *testing.T) {
Context: ctx, Context: ctx,
router: v2.Router(), router: v2.Router(),
driver: driver, driver: driver,
registry: storage.NewRegistryWithDriver(ctx, driver, cache.NewInMemoryLayerInfoCache()), registry: storage.NewRegistryWithDriver(ctx, driver, cache.NewInMemoryBlobDescriptorCacheProvider()),
} }
server := httptest.NewServer(app) server := httptest.NewServer(app)
router := v2.Router() router := v2.Router()

69
docs/handlers/blob.go Normal file
View file

@ -0,0 +1,69 @@
package handlers
import (
"net/http"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
)
// blobDispatcher uses the request context to build a blobHandler.
func blobDispatcher(ctx *Context, r *http.Request) http.Handler {
dgst, err := getDigest(ctx)
if err != nil {
if err == errDigestNotAvailable {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
ctx.Errors.Push(v2.ErrorCodeDigestInvalid, err)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx.Errors.Push(v2.ErrorCodeDigestInvalid, err)
})
}
blobHandler := &blobHandler{
Context: ctx,
Digest: dgst,
}
return handlers.MethodHandler{
"GET": http.HandlerFunc(blobHandler.GetBlob),
"HEAD": http.HandlerFunc(blobHandler.GetBlob),
}
}
// blobHandler serves http blob requests.
type blobHandler struct {
*Context
Digest digest.Digest
}
// GetBlob fetches the binary data from backend storage returns it in the
// response.
func (bh *blobHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
context.GetLogger(bh).Debug("GetBlob")
blobs := bh.Repository.Blobs(bh)
desc, err := blobs.Stat(bh, bh.Digest)
if err != nil {
if err == distribution.ErrBlobUnknown {
w.WriteHeader(http.StatusNotFound)
bh.Errors.Push(v2.ErrorCodeBlobUnknown, bh.Digest)
} else {
bh.Errors.Push(v2.ErrorCodeUnknown, err)
}
return
}
if err := blobs.ServeBlob(bh, w, r, desc.Digest); err != nil {
context.GetLogger(bh).Debugf("unexpected error getting blob HTTP handler: %v", err)
bh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
}

355
docs/handlers/blobupload.go Normal file
View file

@ -0,0 +1,355 @@
package handlers
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"github.com/docker/distribution"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
)
// blobUploadDispatcher constructs and returns the blob upload handler for the
// given request context.
func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
buh := &blobUploadHandler{
Context: ctx,
UUID: getUploadUUID(ctx),
}
handler := http.Handler(handlers.MethodHandler{
"POST": http.HandlerFunc(buh.StartBlobUpload),
"GET": http.HandlerFunc(buh.GetUploadStatus),
"HEAD": http.HandlerFunc(buh.GetUploadStatus),
"PATCH": http.HandlerFunc(buh.PatchBlobData),
"PUT": http.HandlerFunc(buh.PutBlobUploadComplete),
"DELETE": http.HandlerFunc(buh.CancelBlobUpload),
})
if buh.UUID != "" {
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err)
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
buh.State = state
if state.Name != ctx.Repository.Name() {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Name())
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
if state.UUID != buh.UUID {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID)
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
blobs := ctx.Repository.Blobs(buh)
upload, err := blobs.Resume(buh, buh.UUID)
if err != nil {
ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err)
if err == distribution.ErrBlobUploadUnknown {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeBlobUploadUnknown, err)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
buh.Errors.Push(v2.ErrorCodeUnknown, err)
})
}
buh.Upload = upload
if state.Offset > 0 {
// Seek the blob upload to the correct spot if it's non-zero.
// These error conditions should be rare and demonstrate really
// problems. We basically cancel the upload and tell the client to
// start over.
if nn, err := upload.Seek(buh.State.Offset, os.SEEK_SET); err != nil {
defer upload.Close()
ctxu.GetLogger(ctx).Infof("error seeking blob upload: %v", err)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
upload.Cancel(buh)
})
} else if nn != buh.State.Offset {
defer upload.Close()
ctxu.GetLogger(ctx).Infof("seek to wrong offest: %d != %d", nn, buh.State.Offset)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
upload.Cancel(buh)
})
}
}
handler = closeResources(handler, buh.Upload)
}
return handler
}
// blobUploadHandler handles the http blob upload process.
type blobUploadHandler struct {
*Context
// UUID identifies the upload instance for the current request. Using UUID
// to key blob writers since this implementation uses UUIDs.
UUID string
Upload distribution.BlobWriter
State blobUploadState
}
// StartBlobUpload begins the blob upload process and allocates a server-side
// blob writer session.
func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) {
blobs := buh.Repository.Blobs(buh)
upload, err := blobs.Create(buh)
if err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
buh.Upload = upload
defer buh.Upload.Close()
if err := buh.blobUploadResponse(w, r, true); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.Header().Set("Docker-Upload-UUID", buh.Upload.ID())
w.WriteHeader(http.StatusAccepted)
}
// GetUploadStatus returns the status of a given upload, identified by id.
func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
if buh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
// TODO(dmcgowan): Set last argument to false in blobUploadResponse when
// resumable upload is supported. This will enable returning a non-zero
// range for clients to begin uploading at an offset.
if err := buh.blobUploadResponse(w, r, true); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.Header().Set("Docker-Upload-UUID", buh.UUID)
w.WriteHeader(http.StatusNoContent)
}
// PatchBlobData writes data to an upload.
func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) {
if buh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
ct := r.Header.Get("Content-Type")
if ct != "" && ct != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
// TODO(dmcgowan): encode error
return
}
// TODO(dmcgowan): support Content-Range header to seek and write range
// Copy the data
if _, err := io.Copy(buh.Upload, r.Body); err != nil {
ctxu.GetLogger(buh).Errorf("unknown error copying into upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
if err := buh.blobUploadResponse(w, r, false); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.WriteHeader(http.StatusAccepted)
}
// PutBlobUploadComplete takes the final request of a blob upload. The
// request may include all the blob data or no blob data. Any data
// provided is received and verified. If successful, the blob is linked
// into the blob store and 201 Created is returned with the canonical
// url of the blob.
func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) {
if buh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
if dgstStr == "" {
// no digest? return error, but allow retry.
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest missing")
return
}
dgst, err := digest.ParseDigest(dgstStr)
if err != nil {
// no digest? return error, but allow retry.
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest parsing failed")
return
}
// Read in the data, if any.
if _, err := io.Copy(buh.Upload, r.Body); err != nil {
ctxu.GetLogger(buh).Errorf("unknown error copying into upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
buh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
desc, err := buh.Upload.Commit(buh, distribution.Descriptor{
Digest: dgst,
// TODO(stevvooe): This isn't wildly important yet, but we should
// really set the length and mediatype. For now, we can let the
// backend take care of this.
})
if err != nil {
switch err := err.(type) {
case distribution.ErrBlobInvalidDigest:
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeDigestInvalid, err)
default:
switch err {
case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported:
w.WriteHeader(http.StatusBadRequest)
buh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
default:
ctxu.GetLogger(buh).Errorf("unknown error completing upload: %#v", err)
w.WriteHeader(http.StatusInternalServerError)
buh.Errors.Push(v2.ErrorCodeUnknown, err)
}
}
// Clean up the backend blob data if there was an error.
if err := buh.Upload.Cancel(buh); err != nil {
// If the cleanup fails, all we can do is observe and report.
ctxu.GetLogger(buh).Errorf("error canceling upload after error: %v", err)
}
return
}
// Build our canonical blob url
blobURL, err := buh.urlBuilder.BuildBlobURL(buh.Repository.Name(), desc.Digest)
if err != nil {
buh.Errors.Push(v2.ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Location", blobURL)
w.Header().Set("Content-Length", "0")
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
w.WriteHeader(http.StatusCreated)
}
// CancelBlobUpload cancels an in-progress upload of a blob.
func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) {
if buh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
buh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
w.Header().Set("Docker-Upload-UUID", buh.UUID)
if err := buh.Upload.Cancel(buh); err != nil {
ctxu.GetLogger(buh).Errorf("error encountered canceling upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
buh.Errors.PushErr(err)
}
w.WriteHeader(http.StatusNoContent)
}
// blobUploadResponse provides a standard request for uploading blobs and
// chunk responses. This sets the correct headers but the response status is
// left to the caller. The fresh argument is used to ensure that new blob
// uploads always start at a 0 offset. This allows disabling resumable push by
// always returning a 0 offset on check status.
func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
var offset int64
if !fresh {
var err error
offset, err = buh.Upload.Seek(0, os.SEEK_CUR)
if err != nil {
ctxu.GetLogger(buh).Errorf("unable get current offset of blob upload: %v", err)
return err
}
}
// TODO(stevvooe): Need a better way to manage the upload state automatically.
buh.State.Name = buh.Repository.Name()
buh.State.UUID = buh.Upload.ID()
buh.State.Offset = offset
buh.State.StartedAt = buh.Upload.StartedAt()
token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State)
if err != nil {
ctxu.GetLogger(buh).Infof("error building upload state token: %s", err)
return err
}
uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL(
buh.Repository.Name(), buh.Upload.ID(),
url.Values{
"_state": []string{token},
})
if err != nil {
ctxu.GetLogger(buh).Infof("error building upload url: %s", err)
return err
}
endRange := offset
if endRange > 0 {
endRange = endRange - 1
}
w.Header().Set("Docker-Upload-UUID", buh.UUID)
w.Header().Set("Location", uploadURL)
w.Header().Set("Content-Length", "0")
w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
return nil
}

View file

@ -9,9 +9,9 @@ import (
"time" "time"
) )
// layerUploadState captures the state serializable state of the layer upload. // blobUploadState captures the state serializable state of the blob upload.
type layerUploadState struct { type blobUploadState struct {
// name is the primary repository under which the layer will be linked. // name is the primary repository under which the blob will be linked.
Name string Name string
// UUID identifies the upload. // UUID identifies the upload.
@ -26,10 +26,10 @@ type layerUploadState struct {
type hmacKey string type hmacKey string
// unpackUploadState unpacks and validates the layer upload state from the // unpackUploadState unpacks and validates the blob upload state from the
// token, using the hmacKey secret. // token, using the hmacKey secret.
func (secret hmacKey) unpackUploadState(token string) (layerUploadState, error) { func (secret hmacKey) unpackUploadState(token string) (blobUploadState, error) {
var state layerUploadState var state blobUploadState
tokenBytes, err := base64.URLEncoding.DecodeString(token) tokenBytes, err := base64.URLEncoding.DecodeString(token)
if err != nil { if err != nil {
@ -59,7 +59,7 @@ func (secret hmacKey) unpackUploadState(token string) (layerUploadState, error)
// packUploadState packs the upload state signed with and hmac digest using // packUploadState packs the upload state signed with and hmac digest using
// the hmacKey secret, encoding to url safe base64. The resulting token can be // the hmacKey secret, encoding to url safe base64. The resulting token can be
// used to share data with minimized risk of external tampering. // used to share data with minimized risk of external tampering.
func (secret hmacKey) packUploadState(lus layerUploadState) (string, error) { func (secret hmacKey) packUploadState(lus blobUploadState) (string, error) {
mac := hmac.New(sha256.New, []byte(secret)) mac := hmac.New(sha256.New, []byte(secret))
p, err := json.Marshal(lus) p, err := json.Marshal(lus)
if err != nil { if err != nil {

View file

@ -2,7 +2,7 @@ package handlers
import "testing" import "testing"
var layerUploadStates = []layerUploadState{ var blobUploadStates = []blobUploadState{
{ {
Name: "hello", Name: "hello",
UUID: "abcd-1234-qwer-0987", UUID: "abcd-1234-qwer-0987",
@ -45,7 +45,7 @@ var secrets = []string{
func TestLayerUploadTokens(t *testing.T) { func TestLayerUploadTokens(t *testing.T) {
secret := hmacKey("supersecret") secret := hmacKey("supersecret")
for _, testcase := range layerUploadStates { for _, testcase := range blobUploadStates {
token, err := secret.packUploadState(testcase) token, err := secret.packUploadState(testcase)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -56,7 +56,7 @@ func TestLayerUploadTokens(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
assertLayerUploadStateEquals(t, testcase, lus) assertBlobUploadStateEquals(t, testcase, lus)
} }
} }
@ -68,7 +68,7 @@ func TestHMACValidation(t *testing.T) {
secret2 := hmacKey(secret) secret2 := hmacKey(secret)
badSecret := hmacKey("DifferentSecret") badSecret := hmacKey("DifferentSecret")
for _, testcase := range layerUploadStates { for _, testcase := range blobUploadStates {
token, err := secret1.packUploadState(testcase) token, err := secret1.packUploadState(testcase)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -79,7 +79,7 @@ func TestHMACValidation(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
assertLayerUploadStateEquals(t, testcase, lus) assertBlobUploadStateEquals(t, testcase, lus)
_, err = badSecret.unpackUploadState(token) _, err = badSecret.unpackUploadState(token)
if err == nil { if err == nil {
@ -104,7 +104,7 @@ func TestHMACValidation(t *testing.T) {
} }
} }
func assertLayerUploadStateEquals(t *testing.T, expected layerUploadState, received layerUploadState) { func assertBlobUploadStateEquals(t *testing.T, expected blobUploadState, received blobUploadState) {
if expected.Name != received.Name { if expected.Name != received.Name {
t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name) t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name)
} }

View file

@ -136,14 +136,12 @@ func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http
case distribution.ErrManifestVerification: case distribution.ErrManifestVerification:
for _, verificationError := range err { for _, verificationError := range err {
switch verificationError := verificationError.(type) { switch verificationError := verificationError.(type) {
case distribution.ErrUnknownLayer: case distribution.ErrManifestBlobUnknown:
imh.Errors.Push(v2.ErrorCodeBlobUnknown, verificationError.FSLayer) imh.Errors.Push(v2.ErrorCodeBlobUnknown, verificationError.Digest)
case distribution.ErrManifestUnverified: case distribution.ErrManifestUnverified:
imh.Errors.Push(v2.ErrorCodeManifestUnverified) imh.Errors.Push(v2.ErrorCodeManifestUnverified)
default: default:
if verificationError == digest.ErrDigestInvalidFormat { if verificationError == digest.ErrDigestInvalidFormat {
// TODO(stevvooe): We need to really need to move all
// errors to types. Its much more straightforward.
imh.Errors.Push(v2.ErrorCodeDigestInvalid) imh.Errors.Push(v2.ErrorCodeDigestInvalid)
} else { } else {
imh.Errors.PushErr(verificationError) imh.Errors.PushErr(verificationError)

View file

@ -1,74 +0,0 @@
package handlers
import (
"net/http"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
)
// layerDispatcher uses the request context to build a layerHandler.
func layerDispatcher(ctx *Context, r *http.Request) http.Handler {
dgst, err := getDigest(ctx)
if err != nil {
if err == errDigestNotAvailable {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
ctx.Errors.Push(v2.ErrorCodeDigestInvalid, err)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx.Errors.Push(v2.ErrorCodeDigestInvalid, err)
})
}
layerHandler := &layerHandler{
Context: ctx,
Digest: dgst,
}
return handlers.MethodHandler{
"GET": http.HandlerFunc(layerHandler.GetLayer),
"HEAD": http.HandlerFunc(layerHandler.GetLayer),
}
}
// layerHandler serves http layer requests.
type layerHandler struct {
*Context
Digest digest.Digest
}
// GetLayer fetches the binary data from backend storage returns it in the
// response.
func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
context.GetLogger(lh).Debug("GetImageLayer")
layers := lh.Repository.Layers()
layer, err := layers.Fetch(lh.Digest)
if err != nil {
switch err := err.(type) {
case distribution.ErrUnknownLayer:
w.WriteHeader(http.StatusNotFound)
lh.Errors.Push(v2.ErrorCodeBlobUnknown, err.FSLayer)
default:
lh.Errors.Push(v2.ErrorCodeUnknown, err)
}
return
}
handler, err := layer.Handler(r)
if err != nil {
context.GetLogger(lh).Debugf("unexpected error getting layer HTTP handler: %s", err)
lh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
handler.ServeHTTP(w, r)
}

View file

@ -1,344 +0,0 @@
package handlers
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"github.com/docker/distribution"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/api/v2"
"github.com/gorilla/handlers"
)
// layerUploadDispatcher constructs and returns the layer upload handler for
// the given request context.
func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
luh := &layerUploadHandler{
Context: ctx,
UUID: getUploadUUID(ctx),
}
handler := http.Handler(handlers.MethodHandler{
"POST": http.HandlerFunc(luh.StartLayerUpload),
"GET": http.HandlerFunc(luh.GetUploadStatus),
"HEAD": http.HandlerFunc(luh.GetUploadStatus),
"PATCH": http.HandlerFunc(luh.PatchLayerData),
"PUT": http.HandlerFunc(luh.PutLayerUploadComplete),
"DELETE": http.HandlerFunc(luh.CancelLayerUpload),
})
if luh.UUID != "" {
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err)
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
luh.State = state
if state.Name != ctx.Repository.Name() {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, luh.Repository.Name())
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
if state.UUID != luh.UUID {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, luh.UUID)
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
})
}
layers := ctx.Repository.Layers()
upload, err := layers.Resume(luh.UUID)
if err != nil {
ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err)
if err == distribution.ErrLayerUploadUnknown {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown, err)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(v2.ErrorCodeUnknown, err)
})
}
luh.Upload = upload
if state.Offset > 0 {
// Seek the layer upload to the correct spot if it's non-zero.
// These error conditions should be rare and demonstrate really
// problems. We basically cancel the upload and tell the client to
// start over.
if nn, err := upload.Seek(luh.State.Offset, os.SEEK_SET); err != nil {
defer upload.Close()
ctxu.GetLogger(ctx).Infof("error seeking layer upload: %v", err)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
upload.Cancel()
})
} else if nn != luh.State.Offset {
defer upload.Close()
ctxu.GetLogger(ctx).Infof("seek to wrong offest: %d != %d", nn, luh.State.Offset)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeBlobUploadInvalid, err)
upload.Cancel()
})
}
}
handler = closeResources(handler, luh.Upload)
}
return handler
}
// layerUploadHandler handles the http layer upload process.
type layerUploadHandler struct {
*Context
// UUID identifies the upload instance for the current request.
UUID string
Upload distribution.LayerUpload
State layerUploadState
}
// StartLayerUpload begins the layer upload process and allocates a server-
// side upload session.
func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.Request) {
layers := luh.Repository.Layers()
upload, err := layers.Upload()
if err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
luh.Upload = upload
defer luh.Upload.Close()
if err := luh.layerUploadResponse(w, r, true); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.Header().Set("Docker-Upload-UUID", luh.Upload.UUID())
w.WriteHeader(http.StatusAccepted)
}
// GetUploadStatus returns the status of a given upload, identified by uuid.
func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
// TODO(dmcgowan): Set last argument to false in layerUploadResponse when
// resumable upload is supported. This will enable returning a non-zero
// range for clients to begin uploading at an offset.
if err := luh.layerUploadResponse(w, r, true); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.Header().Set("Docker-Upload-UUID", luh.UUID)
w.WriteHeader(http.StatusNoContent)
}
// PatchLayerData writes data to an upload.
func (luh *layerUploadHandler) PatchLayerData(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
ct := r.Header.Get("Content-Type")
if ct != "" && ct != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
// TODO(dmcgowan): encode error
return
}
// TODO(dmcgowan): support Content-Range header to seek and write range
// Copy the data
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
if err := luh.layerUploadResponse(w, r, false); err != nil {
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
w.WriteHeader(http.StatusAccepted)
}
// PutLayerUploadComplete takes the final request of a layer upload. The
// request may include all the layer data or no layer data. Any data
// provided is received and verified. If successful, the layer is linked
// into the blob store and 201 Created is returned with the canonical
// url of the layer.
func (luh *layerUploadHandler) PutLayerUploadComplete(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
if dgstStr == "" {
// no digest? return error, but allow retry.
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest missing")
return
}
dgst, err := digest.ParseDigest(dgstStr)
if err != nil {
// no digest? return error, but allow retry.
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeDigestInvalid, "digest parsing failed")
return
}
// TODO(stevvooe): Consider checking the error on this copy.
// Theoretically, problems should be detected during verification but we
// may miss a root cause.
// Read in the data, if any.
if _, err := io.Copy(luh.Upload, r.Body); err != nil {
ctxu.GetLogger(luh).Errorf("unknown error copying into upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(v2.ErrorCodeUnknown, err)
return
}
layer, err := luh.Upload.Finish(dgst)
if err != nil {
switch err := err.(type) {
case distribution.ErrLayerInvalidDigest:
w.WriteHeader(http.StatusBadRequest)
luh.Errors.Push(v2.ErrorCodeDigestInvalid, err)
default:
ctxu.GetLogger(luh).Errorf("unknown error completing upload: %#v", err)
w.WriteHeader(http.StatusInternalServerError)
luh.Errors.Push(v2.ErrorCodeUnknown, err)
}
// Clean up the backend layer data if there was an error.
if err := luh.Upload.Cancel(); err != nil {
// If the cleanup fails, all we can do is observe and report.
ctxu.GetLogger(luh).Errorf("error canceling upload after error: %v", err)
}
return
}
// Build our canonical layer url
layerURL, err := luh.urlBuilder.BuildBlobURL(luh.Repository.Name(), layer.Digest())
if err != nil {
luh.Errors.Push(v2.ErrorCodeUnknown, err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Location", layerURL)
w.Header().Set("Content-Length", "0")
w.Header().Set("Docker-Content-Digest", layer.Digest().String())
w.WriteHeader(http.StatusCreated)
}
// CancelLayerUpload cancels an in-progress upload of a layer.
func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) {
if luh.Upload == nil {
w.WriteHeader(http.StatusNotFound)
luh.Errors.Push(v2.ErrorCodeBlobUploadUnknown)
return
}
w.Header().Set("Docker-Upload-UUID", luh.UUID)
if err := luh.Upload.Cancel(); err != nil {
ctxu.GetLogger(luh).Errorf("error encountered canceling upload: %v", err)
w.WriteHeader(http.StatusInternalServerError)
luh.Errors.PushErr(err)
}
w.WriteHeader(http.StatusNoContent)
}
// layerUploadResponse provides a standard request for uploading layers and
// chunk responses. This sets the correct headers but the response status is
// left to the caller. The fresh argument is used to ensure that new layer
// uploads always start at a 0 offset. This allows disabling resumable push
// by always returning a 0 offset on check status.
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
var offset int64
if !fresh {
var err error
offset, err = luh.Upload.Seek(0, os.SEEK_CUR)
if err != nil {
ctxu.GetLogger(luh).Errorf("unable get current offset of layer upload: %v", err)
return err
}
}
// TODO(stevvooe): Need a better way to manage the upload state automatically.
luh.State.Name = luh.Repository.Name()
luh.State.UUID = luh.Upload.UUID()
luh.State.Offset = offset
luh.State.StartedAt = luh.Upload.StartedAt()
token, err := hmacKey(luh.Config.HTTP.Secret).packUploadState(luh.State)
if err != nil {
ctxu.GetLogger(luh).Infof("error building upload state token: %s", err)
return err
}
uploadURL, err := luh.urlBuilder.BuildBlobUploadChunkURL(
luh.Repository.Name(), luh.Upload.UUID(),
url.Values{
"_state": []string{token},
})
if err != nil {
ctxu.GetLogger(luh).Infof("error building upload url: %s", err)
return err
}
endRange := offset
if endRange > 0 {
endRange = endRange - 1
}
w.Header().Set("Docker-Upload-UUID", luh.UUID)
w.Header().Set("Location", uploadURL)
w.Header().Set("Content-Length", "0")
w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
return nil
}

View file

@ -13,14 +13,13 @@ import (
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache"
storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
) )
// TestSimpleLayerUpload covers the layer upload process, exercising common // TestSimpleBlobUpload covers the blob upload process, exercising common
// error paths that might be seen during an upload. // error paths that might be seen during an upload.
func TestSimpleLayerUpload(t *testing.T) { func TestSimpleBlobUpload(t *testing.T) {
randomDataReader, tarSumStr, err := testutil.CreateRandomTarFile() randomDataReader, tarSumStr, err := testutil.CreateRandomTarFile()
if err != nil { if err != nil {
@ -36,35 +35,35 @@ func TestSimpleLayerUpload(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName := "foo/bar" imageName := "foo/bar"
driver := inmemory.New() driver := inmemory.New()
registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryLayerInfoCache()) registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryBlobDescriptorCacheProvider())
repository, err := registry.Repository(ctx, imageName) repository, err := registry.Repository(ctx, imageName)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting repo: %v", err) t.Fatalf("unexpected error getting repo: %v", err)
} }
ls := repository.Layers() bs := repository.Blobs(ctx)
h := sha256.New() h := sha256.New()
rd := io.TeeReader(randomDataReader, h) rd := io.TeeReader(randomDataReader, h)
layerUpload, err := ls.Upload() blobUpload, err := bs.Create(ctx)
if err != nil { if err != nil {
t.Fatalf("unexpected error starting layer upload: %s", err) t.Fatalf("unexpected error starting layer upload: %s", err)
} }
// Cancel the upload then restart it // Cancel the upload then restart it
if err := layerUpload.Cancel(); err != nil { if err := blobUpload.Cancel(ctx); err != nil {
t.Fatalf("unexpected error during upload cancellation: %v", err) t.Fatalf("unexpected error during upload cancellation: %v", err)
} }
// Do a resume, get unknown upload // Do a resume, get unknown upload
layerUpload, err = ls.Resume(layerUpload.UUID()) blobUpload, err = bs.Resume(ctx, blobUpload.ID())
if err != distribution.ErrLayerUploadUnknown { if err != distribution.ErrBlobUploadUnknown {
t.Fatalf("unexpected error resuming upload, should be unkown: %v", err) t.Fatalf("unexpected error resuming upload, should be unkown: %v", err)
} }
// Restart! // Restart!
layerUpload, err = ls.Upload() blobUpload, err = bs.Create(ctx)
if err != nil { if err != nil {
t.Fatalf("unexpected error starting layer upload: %s", err) t.Fatalf("unexpected error starting layer upload: %s", err)
} }
@ -75,7 +74,7 @@ func TestSimpleLayerUpload(t *testing.T) {
t.Fatalf("error getting seeker size of random data: %v", err) t.Fatalf("error getting seeker size of random data: %v", err)
} }
nn, err := io.Copy(layerUpload, rd) nn, err := io.Copy(blobUpload, rd)
if err != nil { if err != nil {
t.Fatalf("unexpected error uploading layer data: %v", err) t.Fatalf("unexpected error uploading layer data: %v", err)
} }
@ -84,46 +83,51 @@ func TestSimpleLayerUpload(t *testing.T) {
t.Fatalf("layer data write incomplete") t.Fatalf("layer data write incomplete")
} }
offset, err := layerUpload.Seek(0, os.SEEK_CUR) offset, err := blobUpload.Seek(0, os.SEEK_CUR)
if err != nil { if err != nil {
t.Fatalf("unexpected error seeking layer upload: %v", err) t.Fatalf("unexpected error seeking layer upload: %v", err)
} }
if offset != nn { if offset != nn {
t.Fatalf("layerUpload not updated with correct offset: %v != %v", offset, nn) t.Fatalf("blobUpload not updated with correct offset: %v != %v", offset, nn)
} }
layerUpload.Close() blobUpload.Close()
// Do a resume, for good fun // Do a resume, for good fun
layerUpload, err = ls.Resume(layerUpload.UUID()) blobUpload, err = bs.Resume(ctx, blobUpload.ID())
if err != nil { if err != nil {
t.Fatalf("unexpected error resuming upload: %v", err) t.Fatalf("unexpected error resuming upload: %v", err)
} }
sha256Digest := digest.NewDigest("sha256", h) sha256Digest := digest.NewDigest("sha256", h)
layer, err := layerUpload.Finish(dgst) desc, err := blobUpload.Commit(ctx, distribution.Descriptor{Digest: dgst})
if err != nil { if err != nil {
t.Fatalf("unexpected error finishing layer upload: %v", err) t.Fatalf("unexpected error finishing layer upload: %v", err)
} }
// After finishing an upload, it should no longer exist. // After finishing an upload, it should no longer exist.
if _, err := ls.Resume(layerUpload.UUID()); err != distribution.ErrLayerUploadUnknown { if _, err := bs.Resume(ctx, blobUpload.ID()); err != distribution.ErrBlobUploadUnknown {
t.Fatalf("expected layer upload to be unknown, got %v", err) t.Fatalf("expected layer upload to be unknown, got %v", err)
} }
// Test for existence. // Test for existence.
exists, err := ls.Exists(layer.Digest()) statDesc, err := bs.Stat(ctx, desc.Digest)
if err != nil { if err != nil {
t.Fatalf("unexpected error checking for existence: %v", err) t.Fatalf("unexpected error checking for existence: %v, %#v", err, bs)
} }
if !exists { if statDesc != desc {
t.Fatalf("layer should now exist") t.Fatalf("descriptors not equal: %v != %v", statDesc, desc)
} }
rc, err := bs.Open(ctx, desc.Digest)
if err != nil {
t.Fatalf("unexpected error opening blob for read: %v", err)
}
defer rc.Close()
h.Reset() h.Reset()
nn, err = io.Copy(h, layer) nn, err = io.Copy(h, rc)
if err != nil { if err != nil {
t.Fatalf("error reading layer: %v", err) t.Fatalf("error reading layer: %v", err)
} }
@ -137,21 +141,21 @@ func TestSimpleLayerUpload(t *testing.T) {
} }
} }
// TestSimpleLayerRead just creates a simple layer file and ensures that basic // TestSimpleBlobRead just creates a simple blob file and ensures that basic
// open, read, seek, read works. More specific edge cases should be covered in // open, read, seek, read works. More specific edge cases should be covered in
// other tests. // other tests.
func TestSimpleLayerRead(t *testing.T) { func TestSimpleBlobRead(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName := "foo/bar" imageName := "foo/bar"
driver := inmemory.New() driver := inmemory.New()
registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryLayerInfoCache()) registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryBlobDescriptorCacheProvider())
repository, err := registry.Repository(ctx, imageName) repository, err := registry.Repository(ctx, imageName)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting repo: %v", err) t.Fatalf("unexpected error getting repo: %v", err)
} }
ls := repository.Layers() bs := repository.Blobs(ctx)
randomLayerReader, tarSumStr, err := testutil.CreateRandomTarFile() randomLayerReader, tarSumStr, err := testutil.CreateRandomTarFile() // TODO(stevvooe): Consider using just a random string.
if err != nil { if err != nil {
t.Fatalf("error creating random data: %v", err) t.Fatalf("error creating random data: %v", err)
} }
@ -159,31 +163,14 @@ func TestSimpleLayerRead(t *testing.T) {
dgst := digest.Digest(tarSumStr) dgst := digest.Digest(tarSumStr)
// Test for existence. // Test for existence.
exists, err := ls.Exists(dgst) desc, err := bs.Stat(ctx, dgst)
if err != nil { if err != distribution.ErrBlobUnknown {
t.Fatalf("unexpected error checking for existence: %v", err) t.Fatalf("expected not found error when testing for existence: %v", err)
} }
if exists { rc, err := bs.Open(ctx, dgst)
t.Fatalf("layer should not exist") if err != distribution.ErrBlobUnknown {
} t.Fatalf("expected not found error when opening non-existent blob: %v", err)
// Try to get the layer and make sure we get a not found error
layer, err := ls.Fetch(dgst)
if err == nil {
t.Fatalf("error expected fetching unknown layer")
}
switch err.(type) {
case distribution.ErrUnknownLayer:
err = nil
default:
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
}
randomLayerDigest, err := writeTestLayer(driver, defaultPathMapper, imageName, dgst, randomLayerReader)
if err != nil {
t.Fatalf("unexpected error writing test layer: %v", err)
} }
randomLayerSize, err := seekerSize(randomLayerReader) randomLayerSize, err := seekerSize(randomLayerReader)
@ -191,45 +178,57 @@ func TestSimpleLayerRead(t *testing.T) {
t.Fatalf("error getting seeker size for random layer: %v", err) t.Fatalf("error getting seeker size for random layer: %v", err)
} }
layer, err = ls.Fetch(dgst) descBefore := distribution.Descriptor{Digest: dgst, MediaType: "application/octet-stream", Length: randomLayerSize}
t.Logf("desc: %v", descBefore)
desc, err = addBlob(ctx, bs, descBefore, randomLayerReader)
if err != nil { if err != nil {
t.Fatal(err) t.Fatalf("error adding blob to blobservice: %v", err)
} }
defer layer.Close()
if desc.Length != randomLayerSize {
t.Fatalf("committed blob has incorrect length: %v != %v", desc.Length, randomLayerSize)
}
rc, err = bs.Open(ctx, desc.Digest) // note that we are opening with original digest.
if err != nil {
t.Fatalf("error opening blob with %v: %v", dgst, err)
}
defer rc.Close()
// Now check the sha digest and ensure its the same // Now check the sha digest and ensure its the same
h := sha256.New() h := sha256.New()
nn, err := io.Copy(h, layer) nn, err := io.Copy(h, rc)
if err != nil && err != io.EOF { if err != nil {
t.Fatalf("unexpected error copying to hash: %v", err) t.Fatalf("unexpected error copying to hash: %v", err)
} }
if nn != randomLayerSize { if nn != randomLayerSize {
t.Fatalf("stored incorrect number of bytes in layer: %d != %d", nn, randomLayerSize) t.Fatalf("stored incorrect number of bytes in blob: %d != %d", nn, randomLayerSize)
} }
sha256Digest := digest.NewDigest("sha256", h) sha256Digest := digest.NewDigest("sha256", h)
if sha256Digest != randomLayerDigest { if sha256Digest != desc.Digest {
t.Fatalf("fetched digest does not match: %q != %q", sha256Digest, randomLayerDigest) t.Fatalf("fetched digest does not match: %q != %q", sha256Digest, desc.Digest)
} }
// Now seek back the layer, read the whole thing and check against randomLayerData // Now seek back the blob, read the whole thing and check against randomLayerData
offset, err := layer.Seek(0, os.SEEK_SET) offset, err := rc.Seek(0, os.SEEK_SET)
if err != nil { if err != nil {
t.Fatalf("error seeking layer: %v", err) t.Fatalf("error seeking blob: %v", err)
} }
if offset != 0 { if offset != 0 {
t.Fatalf("seek failed: expected 0 offset, got %d", offset) t.Fatalf("seek failed: expected 0 offset, got %d", offset)
} }
p, err := ioutil.ReadAll(layer) p, err := ioutil.ReadAll(rc)
if err != nil { if err != nil {
t.Fatalf("error reading all of layer: %v", err) t.Fatalf("error reading all of blob: %v", err)
} }
if len(p) != int(randomLayerSize) { if len(p) != int(randomLayerSize) {
t.Fatalf("layer data read has different length: %v != %v", len(p), randomLayerSize) t.Fatalf("blob data read has different length: %v != %v", len(p), randomLayerSize)
} }
// Reset the randomLayerReader and read back the buffer // Reset the randomLayerReader and read back the buffer
@ -253,19 +252,26 @@ func TestLayerUploadZeroLength(t *testing.T) {
ctx := context.Background() ctx := context.Background()
imageName := "foo/bar" imageName := "foo/bar"
driver := inmemory.New() driver := inmemory.New()
registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryLayerInfoCache()) registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryBlobDescriptorCacheProvider())
repository, err := registry.Repository(ctx, imageName) repository, err := registry.Repository(ctx, imageName)
if err != nil { if err != nil {
t.Fatalf("unexpected error getting repo: %v", err) t.Fatalf("unexpected error getting repo: %v", err)
} }
ls := repository.Layers() bs := repository.Blobs(ctx)
upload, err := ls.Upload() wr, err := bs.Create(ctx)
if err != nil { if err != nil {
t.Fatalf("unexpected error starting upload: %v", err) t.Fatalf("unexpected error starting upload: %v", err)
} }
io.Copy(upload, bytes.NewReader([]byte{})) nn, err := io.Copy(wr, bytes.NewReader([]byte{}))
if err != nil {
t.Fatalf("error copying into blob writer: %v", err)
}
if nn != 0 {
t.Fatalf("unexpected number of bytes copied: %v > 0", nn)
}
dgst, err := digest.FromReader(bytes.NewReader([]byte{})) dgst, err := digest.FromReader(bytes.NewReader([]byte{}))
if err != nil { if err != nil {
@ -277,37 +283,16 @@ func TestLayerUploadZeroLength(t *testing.T) {
t.Fatalf("digest not as expected: %v != %v", dgst, digest.DigestTarSumV1EmptyTar) t.Fatalf("digest not as expected: %v != %v", dgst, digest.DigestTarSumV1EmptyTar)
} }
layer, err := upload.Finish(dgst) desc, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst})
if err != nil { if err != nil {
t.Fatalf("unexpected error finishing upload: %v", err) t.Fatalf("unexpected error committing write: %v", err)
} }
if layer.Digest() != dgst { if desc.Digest != dgst {
t.Fatalf("unexpected digest: %v != %v", layer.Digest(), dgst) t.Fatalf("unexpected digest: %v != %v", desc.Digest, dgst)
} }
} }
// writeRandomLayer creates a random layer under name and tarSum using driver
// and pathMapper. An io.ReadSeeker with the data is returned, along with the
// sha256 hex digest.
func writeRandomLayer(driver storagedriver.StorageDriver, pathMapper *pathMapper, name string) (rs io.ReadSeeker, tarSum digest.Digest, sha256digest digest.Digest, err error) {
reader, tarSumStr, err := testutil.CreateRandomTarFile()
if err != nil {
return nil, "", "", err
}
tarSum = digest.Digest(tarSumStr)
// Now, actually create the layer.
randomLayerDigest, err := writeTestLayer(driver, pathMapper, name, tarSum, ioutil.NopCloser(reader))
if _, err := reader.Seek(0, os.SEEK_SET); err != nil {
return nil, "", "", err
}
return reader, tarSum, randomLayerDigest, err
}
// seekerSize seeks to the end of seeker, checks the size and returns it to // seekerSize seeks to the end of seeker, checks the size and returns it to
// the original state, returning the size. The state of the seeker should be // the original state, returning the size. The state of the seeker should be
// treated as unknown if an error is returned. // treated as unknown if an error is returned.
@ -334,46 +319,20 @@ func seekerSize(seeker io.ReadSeeker) (int64, error) {
return end, nil return end, nil
} }
// createTestLayer creates a simple test layer in the provided driver under // addBlob simply consumes the reader and inserts into the blob service,
// tarsum dgst, returning the sha256 digest location. This is implemented // returning a descriptor on success.
// piecemeal and should probably be replaced by the uploader when it's ready. func addBlob(ctx context.Context, bs distribution.BlobIngester, desc distribution.Descriptor, rd io.Reader) (distribution.Descriptor, error) {
func writeTestLayer(driver storagedriver.StorageDriver, pathMapper *pathMapper, name string, dgst digest.Digest, content io.Reader) (digest.Digest, error) { wr, err := bs.Create(ctx)
h := sha256.New()
rd := io.TeeReader(content, h)
p, err := ioutil.ReadAll(rd)
if err != nil { if err != nil {
return "", nil return distribution.Descriptor{}, err
}
defer wr.Cancel(ctx)
if nn, err := io.Copy(wr, rd); err != nil {
return distribution.Descriptor{}, err
} else if nn != desc.Length {
return distribution.Descriptor{}, fmt.Errorf("incorrect number of bytes copied: %v != %v", nn, desc.Length)
} }
blobDigestSHA := digest.NewDigest("sha256", h) return wr.Commit(ctx, desc)
blobPath, err := pathMapper.path(blobDataPathSpec{
digest: dgst,
})
ctx := context.Background()
if err := driver.PutContent(ctx, blobPath, p); err != nil {
return "", err
}
if err != nil {
return "", err
}
layerLinkPath, err := pathMapper.path(layerLinkPathSpec{
name: name,
digest: dgst,
})
if err != nil {
return "", err
}
if err := driver.PutContent(ctx, layerLinkPath, []byte(dgst)); err != nil {
return "", nil
}
return blobDigestSHA, err
} }

View file

@ -0,0 +1,72 @@
package storage
import (
"fmt"
"net/http"
"time"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/driver"
)
// TODO(stevvooe): This should configurable in the future.
const blobCacheControlMaxAge = 365 * 24 * time.Hour
// blobServer simply serves blobs from a driver instance using a path function
// to identify paths and a descriptor service to fill in metadata.
type blobServer struct {
driver driver.StorageDriver
statter distribution.BlobStatter
pathFn func(dgst digest.Digest) (string, error)
}
func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
desc, err := bs.statter.Stat(ctx, dgst)
if err != nil {
return err
}
path, err := bs.pathFn(desc.Digest)
if err != nil {
return err
}
redirectURL, err := bs.driver.URLFor(ctx, path, map[string]interface{}{"method": r.Method})
switch err {
case nil:
// Redirect to storage URL.
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
case driver.ErrUnsupportedMethod:
// Fallback to serving the content directly.
br, err := newFileReader(ctx, bs.driver, path, desc.Length)
if err != nil {
return err
}
defer br.Close()
w.Header().Set("ETag", desc.Digest.String()) // If-None-Match handled by ServeContent
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds()))
if w.Header().Get("Docker-Content-Digest") == "" {
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
}
if w.Header().Get("Content-Type") == "" {
// Set the content type if not already set.
w.Header().Set("Content-Type", desc.MediaType)
}
if w.Header().Get("Content-Length") == "" {
// Set the content length if not already set.
w.Header().Set("Content-Length", fmt.Sprint(desc.Length))
}
http.ServeContent(w, r, desc.Digest.String(), time.Time{}, br)
}
// Some unexpected error.
return err
}

View file

@ -1,133 +1,94 @@
package storage package storage
import ( import (
"fmt" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver"
) )
// TODO(stevvooe): Currently, the blobStore implementation used by the // blobStore implements a the read side of the blob store interface over a
// manifest store. The layer store should be refactored to better leverage the // driver without enforcing per-repository membership. This object is
// blobStore, reducing duplicated code. // intentionally a leaky abstraction, providing utility methods that support
// creating and traversing backend links.
// blobStore implements a generalized blob store over a driver, supporting the
// read side and link management. This object is intentionally a leaky
// abstraction, providing utility methods that support creating and traversing
// backend links.
type blobStore struct { type blobStore struct {
driver storagedriver.StorageDriver driver driver.StorageDriver
pm *pathMapper pm *pathMapper
ctx context.Context statter distribution.BlobStatter
} }
// exists reports whether or not the path exists. If the driver returns error var _ distribution.BlobProvider = &blobStore{}
// other than storagedriver.PathNotFound, an error may be returned.
func (bs *blobStore) exists(dgst digest.Digest) (bool, error) {
path, err := bs.path(dgst)
if err != nil { // Get implements the BlobReadService.Get call.
return false, err func (bs *blobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
}
ok, err := exists(bs.ctx, bs.driver, path)
if err != nil {
return false, err
}
return ok, nil
}
// get retrieves the blob by digest, returning it a byte slice. This should
// only be used for small objects.
func (bs *blobStore) get(dgst digest.Digest) ([]byte, error) {
bp, err := bs.path(dgst) bp, err := bs.path(dgst)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bs.driver.GetContent(bs.ctx, bp) p, err := bs.driver.GetContent(ctx, bp)
} if err != nil {
switch err.(type) {
case driver.PathNotFoundError:
return nil, distribution.ErrBlobUnknown
}
// link links the path to the provided digest by writing the digest into the return nil, err
// target file.
func (bs *blobStore) link(path string, dgst digest.Digest) error {
if exists, err := bs.exists(dgst); err != nil {
return err
} else if !exists {
return fmt.Errorf("cannot link non-existent blob")
} }
// The contents of the "link" file are the exact string contents of the return p, err
// digest, which is specified in that package.
return bs.driver.PutContent(bs.ctx, path, []byte(dgst))
} }
// linked reads the link at path and returns the content. func (bs *blobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
func (bs *blobStore) linked(path string) ([]byte, error) { desc, err := bs.statter.Stat(ctx, dgst)
linked, err := bs.readlink(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bs.get(linked) path, err := bs.path(desc.Digest)
if err != nil {
return nil, err
}
return newFileReader(ctx, bs.driver, path, desc.Length)
} }
// readlink returns the linked digest at path. // Put stores the content p in the blob store, calculating the digest. If the
func (bs *blobStore) readlink(path string) (digest.Digest, error) {
content, err := bs.driver.GetContent(bs.ctx, path)
if err != nil {
return "", err
}
linked, err := digest.ParseDigest(string(content))
if err != nil {
return "", err
}
if exists, err := bs.exists(linked); err != nil {
return "", err
} else if !exists {
return "", fmt.Errorf("link %q invalid: blob %s does not exist", path, linked)
}
return linked, nil
}
// resolve reads the digest link at path and returns the blob store link.
func (bs *blobStore) resolve(path string) (string, error) {
dgst, err := bs.readlink(path)
if err != nil {
return "", err
}
return bs.path(dgst)
}
// put stores the content p in the blob store, calculating the digest. If the
// content is already present, only the digest will be returned. This should // content is already present, only the digest will be returned. This should
// only be used for small objects, such as manifests. // only be used for small objects, such as manifests. This implemented as a convenience for other Put implementations
func (bs *blobStore) put(p []byte) (digest.Digest, error) { func (bs *blobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
dgst, err := digest.FromBytes(p) dgst, err := digest.FromBytes(p)
if err != nil { if err != nil {
context.GetLogger(bs.ctx).Errorf("error digesting content: %v, %s", err, string(p)) context.GetLogger(ctx).Errorf("blobStore: error digesting content: %v, %s", err, string(p))
return "", err return distribution.Descriptor{}, err
}
desc, err := bs.statter.Stat(ctx, dgst)
if err == nil {
// content already present
return desc, nil
} else if err != distribution.ErrBlobUnknown {
context.GetLogger(ctx).Errorf("blobStore: error stating content (%v): %#v", dgst, err)
// real error, return it
return distribution.Descriptor{}, err
} }
bp, err := bs.path(dgst) bp, err := bs.path(dgst)
if err != nil { if err != nil {
return "", err return distribution.Descriptor{}, err
} }
// If the content already exists, just return the digest. // TODO(stevvooe): Write out mediatype here, as well.
if exists, err := bs.exists(dgst); err != nil {
return "", err
} else if exists {
return dgst, nil
}
return dgst, bs.driver.PutContent(bs.ctx, bp, p) return distribution.Descriptor{
Length: int64(len(p)),
// NOTE(stevvooe): The central blob store firewalls media types from
// other users. The caller should look this up and override the value
// for the specific repository.
MediaType: "application/octet-stream",
Digest: dgst,
}, bs.driver.PutContent(ctx, bp, p)
} }
// path returns the canonical path for the blob identified by digest. The blob // path returns the canonical path for the blob identified by digest. The blob
@ -144,16 +105,86 @@ func (bs *blobStore) path(dgst digest.Digest) (string, error) {
return bp, nil return bp, nil
} }
// exists provides a utility method to test whether or not a path exists // link links the path to the provided digest by writing the digest into the
func exists(ctx context.Context, driver storagedriver.StorageDriver, path string) (bool, error) { // target file. Caller must ensure that the blob actually exists.
if _, err := driver.Stat(ctx, path); err != nil { func (bs *blobStore) link(ctx context.Context, path string, dgst digest.Digest) error {
// The contents of the "link" file are the exact string contents of the
// digest, which is specified in that package.
return bs.driver.PutContent(ctx, path, []byte(dgst))
}
// readlink returns the linked digest at path.
func (bs *blobStore) readlink(ctx context.Context, path string) (digest.Digest, error) {
content, err := bs.driver.GetContent(ctx, path)
if err != nil {
return "", err
}
linked, err := digest.ParseDigest(string(content))
if err != nil {
return "", err
}
return linked, nil
}
// resolve reads the digest link at path and returns the blob store path.
func (bs *blobStore) resolve(ctx context.Context, path string) (string, error) {
dgst, err := bs.readlink(ctx, path)
if err != nil {
return "", err
}
return bs.path(dgst)
}
type blobStatter struct {
driver driver.StorageDriver
pm *pathMapper
}
var _ distribution.BlobStatter = &blobStatter{}
// Stat implements BlobStatter.Stat by returning the descriptor for the blob
// in the main blob store. If this method returns successfully, there is
// strong guarantee that the blob exists and is available.
func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
path, err := bs.pm.path(blobDataPathSpec{
digest: dgst,
})
if err != nil {
return distribution.Descriptor{}, err
}
fi, err := bs.driver.Stat(ctx, path)
if err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError: case driver.PathNotFoundError:
return false, nil return distribution.Descriptor{}, distribution.ErrBlobUnknown
default: default:
return false, err return distribution.Descriptor{}, err
} }
} }
return true, nil if fi.IsDir() {
// NOTE(stevvooe): This represents a corruption situation. Somehow, we
// calculated a blob path and then detected a directory. We log the
// error and then error on the side of not knowing about the blob.
context.GetLogger(ctx).Warnf("blob path should not be a directory: %q", path)
return distribution.Descriptor{}, distribution.ErrBlobUnknown
}
// TODO(stevvooe): Add method to resolve the mediatype. We can store and
// cache a "global" media type for the blob, even if a specific repo has a
// mediatype that overrides the main one.
return distribution.Descriptor{
Length: fi.Size(),
// NOTE(stevvooe): The central blob store firewalls media types from
// other users. The caller should look this up and override the value
// for the specific repository.
MediaType: "application/octet-stream",
Digest: dgst,
}, nil
} }

469
docs/storage/blobwriter.go Normal file
View file

@ -0,0 +1,469 @@
package storage
import (
"fmt"
"io"
"os"
"path"
"strconv"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
storagedriver "github.com/docker/distribution/registry/storage/driver"
)
// layerWriter is used to control the various aspects of resumable
// layer upload. It implements the LayerUpload interface.
type blobWriter struct {
blobStore *linkedBlobStore
id string
startedAt time.Time
resumableDigester digest.ResumableDigester
// implementes io.WriteSeeker, io.ReaderFrom and io.Closer to satisfy
// LayerUpload Interface
bufferedFileWriter
}
var _ distribution.BlobWriter = &blobWriter{}
// ID returns the identifier for this upload.
func (bw *blobWriter) ID() string {
return bw.id
}
func (bw *blobWriter) StartedAt() time.Time {
return bw.startedAt
}
// Commit marks the upload as completed, returning a valid descriptor. The
// final size and digest are checked against the first descriptor provided.
func (bw *blobWriter) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) {
context.GetLogger(ctx).Debug("(*blobWriter).Commit")
if err := bw.bufferedFileWriter.Close(); err != nil {
return distribution.Descriptor{}, err
}
canonical, err := bw.validateBlob(ctx, desc)
if err != nil {
return distribution.Descriptor{}, err
}
if err := bw.moveBlob(ctx, canonical); err != nil {
return distribution.Descriptor{}, err
}
if err := bw.blobStore.linkBlob(ctx, canonical, desc.Digest); err != nil {
return distribution.Descriptor{}, err
}
if err := bw.removeResources(ctx); err != nil {
return distribution.Descriptor{}, err
}
return canonical, nil
}
// Rollback the blob upload process, releasing any resources associated with
// the writer and canceling the operation.
func (bw *blobWriter) Cancel(ctx context.Context) error {
context.GetLogger(ctx).Debug("(*blobWriter).Rollback")
if err := bw.removeResources(ctx); err != nil {
return err
}
bw.Close()
return nil
}
func (bw *blobWriter) Write(p []byte) (int, error) {
if bw.resumableDigester == nil {
return bw.bufferedFileWriter.Write(p)
}
// Ensure that the current write offset matches how many bytes have been
// written to the digester. If not, we need to update the digest state to
// match the current write position.
if err := bw.resumeHashAt(bw.blobStore.ctx, bw.offset); err != nil {
return 0, err
}
return io.MultiWriter(&bw.bufferedFileWriter, bw.resumableDigester).Write(p)
}
func (bw *blobWriter) ReadFrom(r io.Reader) (n int64, err error) {
if bw.resumableDigester == nil {
return bw.bufferedFileWriter.ReadFrom(r)
}
// Ensure that the current write offset matches how many bytes have been
// written to the digester. If not, we need to update the digest state to
// match the current write position.
if err := bw.resumeHashAt(bw.blobStore.ctx, bw.offset); err != nil {
return 0, err
}
return bw.bufferedFileWriter.ReadFrom(io.TeeReader(r, bw.resumableDigester))
}
func (bw *blobWriter) Close() error {
if bw.err != nil {
return bw.err
}
if bw.resumableDigester != nil {
if err := bw.storeHashState(bw.blobStore.ctx); err != nil {
return err
}
}
return bw.bufferedFileWriter.Close()
}
// validateBlob checks the data against the digest, returning an error if it
// does not match. The canonical descriptor is returned.
func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) {
var (
verified, fullHash bool
canonical digest.Digest
)
if desc.Digest == "" {
// if no descriptors are provided, we have nothing to validate
// against. We don't really want to support this for the registry.
return distribution.Descriptor{}, distribution.ErrBlobInvalidDigest{
Reason: fmt.Errorf("cannot validate against empty digest"),
}
}
// Stat the on disk file
if fi, err := bw.bufferedFileWriter.driver.Stat(ctx, bw.path); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
// NOTE(stevvooe): We really don't care if the file is
// not actually present for the reader. We now assume
// that the desc length is zero.
desc.Length = 0
default:
// Any other error we want propagated up the stack.
return distribution.Descriptor{}, err
}
} else {
if fi.IsDir() {
return distribution.Descriptor{}, fmt.Errorf("unexpected directory at upload location %q", bw.path)
}
bw.size = fi.Size()
}
if desc.Length > 0 {
if desc.Length != bw.size {
return distribution.Descriptor{}, distribution.ErrBlobInvalidLength
}
} else {
// if provided 0 or negative length, we can assume caller doesn't know or
// care about length.
desc.Length = bw.size
}
if bw.resumableDigester != nil {
// Restore the hasher state to the end of the upload.
if err := bw.resumeHashAt(ctx, bw.size); err != nil {
return distribution.Descriptor{}, err
}
canonical = bw.resumableDigester.Digest()
if canonical.Algorithm() == desc.Digest.Algorithm() {
// Common case: client and server prefer the same canonical digest
// algorithm - currently SHA256.
verified = desc.Digest == canonical
} else {
// The client wants to use a different digest algorithm. They'll just
// have to be patient and wait for us to download and re-hash the
// uploaded content using that digest algorithm.
fullHash = true
}
} else {
// Not using resumable digests, so we need to hash the entire layer.
fullHash = true
}
if fullHash {
digester := digest.NewCanonicalDigester()
digestVerifier, err := digest.NewDigestVerifier(desc.Digest)
if err != nil {
return distribution.Descriptor{}, err
}
// Read the file from the backend driver and validate it.
fr, err := newFileReader(ctx, bw.bufferedFileWriter.driver, bw.path, desc.Length)
if err != nil {
return distribution.Descriptor{}, err
}
tr := io.TeeReader(fr, digester)
if _, err := io.Copy(digestVerifier, tr); err != nil {
return distribution.Descriptor{}, err
}
canonical = digester.Digest()
verified = digestVerifier.Verified()
}
if !verified {
context.GetLoggerWithFields(ctx,
map[string]interface{}{
"canonical": canonical,
"provided": desc.Digest,
}, "canonical", "provided").
Errorf("canonical digest does match provided digest")
return distribution.Descriptor{}, distribution.ErrBlobInvalidDigest{
Digest: desc.Digest,
Reason: fmt.Errorf("content does not match digest"),
}
}
// update desc with canonical hash
desc.Digest = canonical
if desc.MediaType == "" {
desc.MediaType = "application/octet-stream"
}
return desc, nil
}
// moveBlob moves the data into its final, hash-qualified destination,
// identified by dgst. The layer should be validated before commencing the
// move.
func (bw *blobWriter) moveBlob(ctx context.Context, desc distribution.Descriptor) error {
blobPath, err := bw.blobStore.pm.path(blobDataPathSpec{
digest: desc.Digest,
})
if err != nil {
return err
}
// Check for existence
if _, err := bw.blobStore.driver.Stat(ctx, blobPath); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
break // ensure that it doesn't exist.
default:
return err
}
} else {
// If the path exists, we can assume that the content has already
// been uploaded, since the blob storage is content-addressable.
// While it may be corrupted, detection of such corruption belongs
// elsewhere.
return nil
}
// If no data was received, we may not actually have a file on disk. Check
// the size here and write a zero-length file to blobPath if this is the
// case. For the most part, this should only ever happen with zero-length
// tars.
if _, err := bw.blobStore.driver.Stat(ctx, bw.path); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
// HACK(stevvooe): This is slightly dangerous: if we verify above,
// get a hash, then the underlying file is deleted, we risk moving
// a zero-length blob into a nonzero-length blob location. To
// prevent this horrid thing, we employ the hack of only allowing
// to this happen for the zero tarsum.
if desc.Digest == digest.DigestSha256EmptyTar {
return bw.blobStore.driver.PutContent(ctx, blobPath, []byte{})
}
// We let this fail during the move below.
logrus.
WithField("upload.id", bw.ID()).
WithField("digest", desc.Digest).Warnf("attempted to move zero-length content with non-zero digest")
default:
return err // unrelated error
}
}
// TODO(stevvooe): We should also write the mediatype when executing this move.
return bw.blobStore.driver.Move(ctx, bw.path, blobPath)
}
type hashStateEntry struct {
offset int64
path string
}
// getStoredHashStates returns a slice of hashStateEntries for this upload.
func (bw *blobWriter) getStoredHashStates(ctx context.Context) ([]hashStateEntry, error) {
uploadHashStatePathPrefix, err := bw.blobStore.pm.path(uploadHashStatePathSpec{
name: bw.blobStore.repository.Name(),
id: bw.id,
alg: bw.resumableDigester.Digest().Algorithm(),
list: true,
})
if err != nil {
return nil, err
}
paths, err := bw.blobStore.driver.List(ctx, uploadHashStatePathPrefix)
if err != nil {
if _, ok := err.(storagedriver.PathNotFoundError); !ok {
return nil, err
}
// Treat PathNotFoundError as no entries.
paths = nil
}
hashStateEntries := make([]hashStateEntry, 0, len(paths))
for _, p := range paths {
pathSuffix := path.Base(p)
// The suffix should be the offset.
offset, err := strconv.ParseInt(pathSuffix, 0, 64)
if err != nil {
logrus.Errorf("unable to parse offset from upload state path %q: %s", p, err)
}
hashStateEntries = append(hashStateEntries, hashStateEntry{offset: offset, path: p})
}
return hashStateEntries, nil
}
// resumeHashAt attempts to restore the state of the internal hash function
// by loading the most recent saved hash state less than or equal to the given
// offset. Any unhashed bytes remaining less than the given offset are hashed
// from the content uploaded so far.
func (bw *blobWriter) resumeHashAt(ctx context.Context, offset int64) error {
if offset < 0 {
return fmt.Errorf("cannot resume hash at negative offset: %d", offset)
}
if offset == int64(bw.resumableDigester.Len()) {
// State of digester is already at the requested offset.
return nil
}
// List hash states from storage backend.
var hashStateMatch hashStateEntry
hashStates, err := bw.getStoredHashStates(ctx)
if err != nil {
return fmt.Errorf("unable to get stored hash states with offset %d: %s", offset, err)
}
// Find the highest stored hashState with offset less than or equal to
// the requested offset.
for _, hashState := range hashStates {
if hashState.offset == offset {
hashStateMatch = hashState
break // Found an exact offset match.
} else if hashState.offset < offset && hashState.offset > hashStateMatch.offset {
// This offset is closer to the requested offset.
hashStateMatch = hashState
} else if hashState.offset > offset {
// Remove any stored hash state with offsets higher than this one
// as writes to this resumed hasher will make those invalid. This
// is probably okay to skip for now since we don't expect anyone to
// use the API in this way. For that reason, we don't treat an
// an error here as a fatal error, but only log it.
if err := bw.driver.Delete(ctx, hashState.path); err != nil {
logrus.Errorf("unable to delete stale hash state %q: %s", hashState.path, err)
}
}
}
if hashStateMatch.offset == 0 {
// No need to load any state, just reset the hasher.
bw.resumableDigester.Reset()
} else {
storedState, err := bw.driver.GetContent(ctx, hashStateMatch.path)
if err != nil {
return err
}
if err = bw.resumableDigester.Restore(storedState); err != nil {
return err
}
}
// Mind the gap.
if gapLen := offset - int64(bw.resumableDigester.Len()); gapLen > 0 {
// Need to read content from the upload to catch up to the desired offset.
fr, err := newFileReader(ctx, bw.driver, bw.path, bw.size)
if err != nil {
return err
}
if _, err = fr.Seek(int64(bw.resumableDigester.Len()), os.SEEK_SET); err != nil {
return fmt.Errorf("unable to seek to layer reader offset %d: %s", bw.resumableDigester.Len(), err)
}
if _, err := io.CopyN(bw.resumableDigester, fr, gapLen); err != nil {
return err
}
}
return nil
}
func (bw *blobWriter) storeHashState(ctx context.Context) error {
uploadHashStatePath, err := bw.blobStore.pm.path(uploadHashStatePathSpec{
name: bw.blobStore.repository.Name(),
id: bw.id,
alg: bw.resumableDigester.Digest().Algorithm(),
offset: int64(bw.resumableDigester.Len()),
})
if err != nil {
return err
}
hashState, err := bw.resumableDigester.State()
if err != nil {
return err
}
return bw.driver.PutContent(ctx, uploadHashStatePath, hashState)
}
// removeResources should clean up all resources associated with the upload
// instance. An error will be returned if the clean up cannot proceed. If the
// resources are already not present, no error will be returned.
func (bw *blobWriter) removeResources(ctx context.Context) error {
dataPath, err := bw.blobStore.pm.path(uploadDataPathSpec{
name: bw.blobStore.repository.Name(),
id: bw.id,
})
if err != nil {
return err
}
// Resolve and delete the containing directory, which should include any
// upload related files.
dirPath := path.Dir(dataPath)
if err := bw.blobStore.driver.Delete(ctx, dirPath); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
break // already gone!
default:
// This should be uncommon enough such that returning an error
// should be okay. At this point, the upload should be mostly
// complete, but perhaps the backend became unaccessible.
context.GetLogger(ctx).Errorf("unable to delete layer upload resources %q: %v", dirPath, err)
return err
}
}
return nil
}

View file

@ -0,0 +1,6 @@
// +build noresumabledigest
package storage
func (bw *blobWriter) setupResumableDigester() {
}

View file

@ -0,0 +1,9 @@
// +build !noresumabledigest
package storage
import "github.com/docker/distribution/digest"
func (bw *blobWriter) setupResumableDigester() {
bw.resumableDigester = digest.NewCanonicalResumableDigester()
}

View file

@ -1,98 +1,38 @@
// Package cache provides facilities to speed up access to the storage // Package cache provides facilities to speed up access to the storage
// backend. Typically cache implementations deal with internal implementation // backend.
// details at the backend level, rather than generalized caches for
// distribution related interfaces. In other words, unless the cache is
// specific to the storage package, it belongs in another package.
package cache package cache
import ( import (
"fmt" "fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"golang.org/x/net/context"
) )
// ErrNotFound is returned when a meta item is not found. // BlobDescriptorCacheProvider provides repository scoped
var ErrNotFound = fmt.Errorf("not found") // BlobDescriptorService cache instances and a global descriptor cache.
type BlobDescriptorCacheProvider interface {
distribution.BlobDescriptorService
// LayerMeta describes the backend location and length of layer data. RepositoryScoped(repo string) (distribution.BlobDescriptorService, error)
type LayerMeta struct {
Path string
Length int64
} }
// LayerInfoCache is a driver-aware cache of layer metadata. Basically, it func validateDigest(dgst digest.Digest) error {
// provides a fast cache for checks against repository metadata, avoiding return dgst.Validate()
// round trips to backend storage. Note that this is different from a pure
// layer cache, which would also provide access to backing data, as well. Such
// a cache should be implemented as a middleware, rather than integrated with
// the storage backend.
//
// Note that most implementations rely on the caller to do strict checks on on
// repo and dgst arguments, since these are mostly used behind existing
// implementations.
type LayerInfoCache interface {
// Contains returns true if the repository with name contains the layer.
Contains(ctx context.Context, repo string, dgst digest.Digest) (bool, error)
// Add includes the layer in the given repository cache.
Add(ctx context.Context, repo string, dgst digest.Digest) error
// Meta provides the location of the layer on the backend and its size. Membership of a
// repository should be tested before using the result, if required.
Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error)
// SetMeta sets the meta data for the given layer.
SetMeta(ctx context.Context, dgst digest.Digest, meta LayerMeta) error
} }
// base implements common checks between cache implementations. Note that func validateDescriptor(desc distribution.Descriptor) error {
// these are not full checks of input, since that should be done by the if err := validateDigest(desc.Digest); err != nil {
// caller. return err
type base struct { }
LayerInfoCache
} if desc.Length < 0 {
return fmt.Errorf("cache: invalid length in descriptor: %v < 0", desc.Length)
func (b *base) Contains(ctx context.Context, repo string, dgst digest.Digest) (bool, error) { }
if repo == "" {
return false, fmt.Errorf("cache: cannot check for empty repository name") if desc.MediaType == "" {
} return fmt.Errorf("cache: empty mediatype on descriptor: %v", desc)
}
if dgst == "" {
return false, fmt.Errorf("cache: cannot check for empty digests") return nil
}
return b.LayerInfoCache.Contains(ctx, repo, dgst)
}
func (b *base) Add(ctx context.Context, repo string, dgst digest.Digest) error {
if repo == "" {
return fmt.Errorf("cache: cannot add empty repository name")
}
if dgst == "" {
return fmt.Errorf("cache: cannot add empty digest")
}
return b.LayerInfoCache.Add(ctx, repo, dgst)
}
func (b *base) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) {
if dgst == "" {
return LayerMeta{}, fmt.Errorf("cache: cannot get meta for empty digest")
}
return b.LayerInfoCache.Meta(ctx, dgst)
}
func (b *base) SetMeta(ctx context.Context, dgst digest.Digest, meta LayerMeta) error {
if dgst == "" {
return fmt.Errorf("cache: cannot set meta for empty digest")
}
if meta.Path == "" {
return fmt.Errorf("cache: cannot set empty path for meta")
}
return b.LayerInfoCache.SetMeta(ctx, dgst, meta)
} }

View file

@ -3,84 +3,139 @@ package cache
import ( import (
"testing" "testing"
"golang.org/x/net/context" "github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
) )
// checkLayerInfoCache takes a cache implementation through a common set of // checkBlobDescriptorCache takes a cache implementation through a common set
// operations. If adding new tests, please add them here so new // of operations. If adding new tests, please add them here so new
// implementations get the benefit. // implementations get the benefit.
func checkLayerInfoCache(t *testing.T, lic LayerInfoCache) { func checkBlobDescriptorCache(t *testing.T, provider BlobDescriptorCacheProvider) {
ctx := context.Background() ctx := context.Background()
exists, err := lic.Contains(ctx, "", "fake:abc") checkBlobDescriptorCacheEmptyRepository(t, ctx, provider)
checkBlobDescriptorCacheSetAndRead(t, ctx, provider)
}
func checkBlobDescriptorCacheEmptyRepository(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
if _, err := provider.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
t.Fatalf("expected unknown blob error with empty store: %v", err)
}
cache, err := provider.RepositoryScoped("")
if err == nil { if err == nil {
t.Fatalf("expected error checking for cache item with empty repo") t.Fatalf("expected an error when asking for invalid repo")
} }
exists, err = lic.Contains(ctx, "foo/bar", "") cache, err = provider.RepositoryScoped("foo/bar")
if err == nil {
t.Fatalf("expected error checking for cache item with empty digest")
}
exists, err = lic.Contains(ctx, "foo/bar", "fake:abc")
if err != nil { if err != nil {
t.Fatalf("unexpected error checking for cache item: %v", err) t.Fatalf("unexpected error getting repository: %v", err)
} }
if exists { if err := cache.SetDescriptor(ctx, "", distribution.Descriptor{
t.Fatalf("item should not exist") Digest: "sha384:abc",
Length: 10,
MediaType: "application/octet-stream"}); err != digest.ErrDigestInvalidFormat {
t.Fatalf("expected error with invalid digest: %v", err)
} }
if err := lic.Add(ctx, "", "fake:abc"); err == nil { if err := cache.SetDescriptor(ctx, "sha384:abc", distribution.Descriptor{
t.Fatalf("expected error adding cache item with empty name") Digest: "",
Length: 10,
MediaType: "application/octet-stream"}); err == nil {
t.Fatalf("expected error setting value on invalid descriptor")
} }
if err := lic.Add(ctx, "foo/bar", ""); err == nil { if _, err := cache.Stat(ctx, ""); err != digest.ErrDigestInvalidFormat {
t.Fatalf("expected error adding cache item with empty digest") t.Fatalf("expected error checking for cache item with empty digest: %v", err)
} }
if err := lic.Add(ctx, "foo/bar", "fake:abc"); err != nil { if _, err := cache.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
t.Fatalf("unexpected error adding item: %v", err) t.Fatalf("expected unknown blob error with empty repo: %v", err)
} }
}
exists, err = lic.Contains(ctx, "foo/bar", "fake:abc")
if err != nil { func checkBlobDescriptorCacheSetAndRead(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
t.Fatalf("unexpected error checking for cache item: %v", err) localDigest := digest.Digest("sha384:abc")
} expected := distribution.Descriptor{
Digest: "sha256:abc",
if !exists { Length: 10,
t.Fatalf("item should exist") MediaType: "application/octet-stream"}
}
cache, err := provider.RepositoryScoped("foo/bar")
_, err = lic.Meta(ctx, "") if err != nil {
if err == nil || err == ErrNotFound { t.Fatalf("unexpected error getting scoped cache: %v", err)
t.Fatalf("expected error getting meta for cache item with empty digest") }
}
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
_, err = lic.Meta(ctx, "fake:abc") t.Fatalf("error setting descriptor: %v", err)
if err != ErrNotFound { }
t.Fatalf("expected unknown layer error getting meta for cache item with empty digest")
} desc, err := cache.Stat(ctx, localDigest)
if err != nil {
if err = lic.SetMeta(ctx, "", LayerMeta{}); err == nil { t.Fatalf("unexpected error statting fake2:abc: %v", err)
t.Fatalf("expected error setting meta for cache item with empty digest") }
}
if expected != desc {
if err = lic.SetMeta(ctx, "foo/bar", LayerMeta{}); err == nil { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
t.Fatalf("expected error setting meta for cache item with empty meta") }
}
// also check that we set the canonical key ("fake:abc")
expected := LayerMeta{Path: "/foo/bar", Length: 20} desc, err = cache.Stat(ctx, localDigest)
if err := lic.SetMeta(ctx, "foo/bar", expected); err != nil { if err != nil {
t.Fatalf("unexpected error setting meta: %v", err) t.Fatalf("descriptor not returned for canonical key: %v", err)
} }
meta, err := lic.Meta(ctx, "foo/bar") if expected != desc {
if err != nil { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
t.Fatalf("unexpected error getting meta: %v", err) }
}
// ensure that global gets extra descriptor mapping
if meta != expected { desc, err = provider.Stat(ctx, localDigest)
t.Fatalf("retrieved meta data did not match: %v", err) if err != nil {
t.Fatalf("expected blob unknown in global cache: %v, %v", err, desc)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// get at it through canonical descriptor
desc, err = provider.Stat(ctx, expected.Digest)
if err != nil {
t.Fatalf("unexpected error checking glboal descriptor: %v", err)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
}
// now, we set the repo local mediatype to something else and ensure it
// doesn't get changed in the provider cache.
expected.MediaType = "application/json"
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
t.Fatalf("unexpected error setting descriptor: %v", err)
}
desc, err = cache.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error getting descriptor: %v", err)
}
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
}
desc, err = provider.Stat(ctx, localDigest)
if err != nil {
t.Fatalf("unexpected error getting global descriptor: %v", err)
}
expected.MediaType = "application/octet-stream" // expect original mediatype in global
if desc != expected {
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
} }
} }

View file

@ -1,63 +1,149 @@
package cache package cache
import ( import (
"sync"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"golang.org/x/net/context" "github.com/docker/distribution/registry/api/v2"
) )
// inmemoryLayerInfoCache is a map-based implementation of LayerInfoCache. type inMemoryBlobDescriptorCacheProvider struct {
type inmemoryLayerInfoCache struct { global *mapBlobDescriptorCache
membership map[string]map[digest.Digest]struct{} repositories map[string]*mapBlobDescriptorCache
meta map[digest.Digest]LayerMeta mu sync.RWMutex
} }
// NewInMemoryLayerInfoCache provides an implementation of LayerInfoCache that // NewInMemoryBlobDescriptorCacheProvider returns a new mapped-based cache for
// stores results in memory. // storing blob descriptor data.
func NewInMemoryLayerInfoCache() LayerInfoCache { func NewInMemoryBlobDescriptorCacheProvider() BlobDescriptorCacheProvider {
return &base{&inmemoryLayerInfoCache{ return &inMemoryBlobDescriptorCacheProvider{
membership: make(map[string]map[digest.Digest]struct{}), global: newMapBlobDescriptorCache(),
meta: make(map[digest.Digest]LayerMeta), repositories: make(map[string]*mapBlobDescriptorCache),
}} }
} }
func (ilic *inmemoryLayerInfoCache) Contains(ctx context.Context, repo string, dgst digest.Digest) (bool, error) { func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
members, ok := ilic.membership[repo] if err := v2.ValidateRespositoryName(repo); err != nil {
if !ok { return nil, err
return false, nil
} }
_, ok = members[dgst] imbdcp.mu.RLock()
return ok, nil defer imbdcp.mu.RUnlock()
return &repositoryScopedInMemoryBlobDescriptorCache{
repo: repo,
parent: imbdcp,
repository: imbdcp.repositories[repo],
}, nil
} }
// Add adds the layer to the redis repository blob set. func (imbdcp *inMemoryBlobDescriptorCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
func (ilic *inmemoryLayerInfoCache) Add(ctx context.Context, repo string, dgst digest.Digest) error { return imbdcp.global.Stat(ctx, dgst)
members, ok := ilic.membership[repo] }
if !ok {
members = make(map[digest.Digest]struct{}) func (imbdcp *inMemoryBlobDescriptorCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
ilic.membership[repo] = members _, err := imbdcp.Stat(ctx, dgst)
if err == distribution.ErrBlobUnknown {
if dgst.Algorithm() != desc.Digest.Algorithm() && dgst != desc.Digest {
// if the digests differ, set the other canonical mapping
if err := imbdcp.global.SetDescriptor(ctx, desc.Digest, desc); err != nil {
return err
}
}
// unknown, just set it
return imbdcp.global.SetDescriptor(ctx, dgst, desc)
} }
members[dgst] = struct{}{} // we already know it, do nothing
return err
}
return nil // repositoryScopedInMemoryBlobDescriptorCache provides the request scoped
} // repository cache. Instances are not thread-safe but the delegated
// operations are.
// Meta retrieves the layer meta data from the redis hash, returning type repositoryScopedInMemoryBlobDescriptorCache struct {
// ErrUnknownLayer if not found. repo string
func (ilic *inmemoryLayerInfoCache) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) { parent *inMemoryBlobDescriptorCacheProvider // allows lazy allocation of repo's map
meta, ok := ilic.meta[dgst] repository *mapBlobDescriptorCache
if !ok { }
return LayerMeta{}, ErrNotFound
} func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
if rsimbdcp.repository == nil {
return meta, nil return distribution.Descriptor{}, distribution.ErrBlobUnknown
} }
// SetMeta sets the meta data for the given digest using a redis hash. A hash return rsimbdcp.repository.Stat(ctx, dgst)
// is used here since we may store unrelated fields about a layer in the }
// future.
func (ilic *inmemoryLayerInfoCache) SetMeta(ctx context.Context, dgst digest.Digest, meta LayerMeta) error { func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
ilic.meta[dgst] = meta if rsimbdcp.repository == nil {
// allocate map since we are setting it now.
rsimbdcp.parent.mu.Lock()
var ok bool
// have to read back value since we may have allocated elsewhere.
rsimbdcp.repository, ok = rsimbdcp.parent.repositories[rsimbdcp.repo]
if !ok {
rsimbdcp.repository = newMapBlobDescriptorCache()
rsimbdcp.parent.repositories[rsimbdcp.repo] = rsimbdcp.repository
}
rsimbdcp.parent.mu.Unlock()
}
if err := rsimbdcp.repository.SetDescriptor(ctx, dgst, desc); err != nil {
return err
}
return rsimbdcp.parent.SetDescriptor(ctx, dgst, desc)
}
// mapBlobDescriptorCache provides a simple map-based implementation of the
// descriptor cache.
type mapBlobDescriptorCache struct {
descriptors map[digest.Digest]distribution.Descriptor
mu sync.RWMutex
}
var _ distribution.BlobDescriptorService = &mapBlobDescriptorCache{}
func newMapBlobDescriptorCache() *mapBlobDescriptorCache {
return &mapBlobDescriptorCache{
descriptors: make(map[digest.Digest]distribution.Descriptor),
}
}
func (mbdc *mapBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
if err := validateDigest(dgst); err != nil {
return distribution.Descriptor{}, err
}
mbdc.mu.RLock()
defer mbdc.mu.RUnlock()
desc, ok := mbdc.descriptors[dgst]
if !ok {
return distribution.Descriptor{}, distribution.ErrBlobUnknown
}
return desc, nil
}
func (mbdc *mapBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
if err := validateDigest(dgst); err != nil {
return err
}
if err := validateDescriptor(desc); err != nil {
return err
}
mbdc.mu.Lock()
defer mbdc.mu.Unlock()
mbdc.descriptors[dgst] = desc
return nil return nil
} }

View file

@ -2,8 +2,8 @@ package cache
import "testing" import "testing"
// TestInMemoryLayerInfoCache checks the in memory implementation is working // TestInMemoryBlobInfoCache checks the in memory implementation is working
// correctly. // correctly.
func TestInMemoryLayerInfoCache(t *testing.T) { func TestInMemoryBlobInfoCache(t *testing.T) {
checkLayerInfoCache(t, NewInMemoryLayerInfoCache()) checkBlobDescriptorCache(t, NewInMemoryBlobDescriptorCacheProvider())
} }

View file

@ -1,20 +1,28 @@
package cache package cache
import ( import (
ctxu "github.com/docker/distribution/context" "fmt"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
"golang.org/x/net/context"
) )
// redisLayerInfoCache provides an implementation of storage.LayerInfoCache // redisBlobStatService provides an implementation of
// based on redis. Layer info is stored in two parts. The first provide fast // BlobDescriptorCacheProvider based on redis. Blob descritors are stored in
// access to repository membership through a redis set for each repo. The // two parts. The first provide fast access to repository membership through a
// second is a redis hash keyed by the digest of the layer, providing path and // redis set for each repo. The second is a redis hash keyed by the digest of
// length information. Note that there is no implied relationship between // the layer, providing path, length and mediatype information. There is also
// these two caches. The layer may exist in one, both or none and the code // a per-repository redis hash of the blob descriptor, allowing override of
// must be written this way. // data. This is currently used to override the mediatype on a per-repository
type redisLayerInfoCache struct { // basis.
//
// Note that there is no implied relationship between these two caches. The
// layer may exist in one, both or none and the code must be written this way.
type redisBlobDescriptorService struct {
pool *redis.Pool pool *redis.Pool
// TODO(stevvooe): We use a pool because we don't have great control over // TODO(stevvooe): We use a pool because we don't have great control over
@ -23,76 +31,194 @@ type redisLayerInfoCache struct {
// request objects, we can change this to a connection. // request objects, we can change this to a connection.
} }
// NewRedisLayerInfoCache returns a new redis-based LayerInfoCache using the var _ BlobDescriptorCacheProvider = &redisBlobDescriptorService{}
// provided redis connection pool.
func NewRedisLayerInfoCache(pool *redis.Pool) LayerInfoCache { // NewRedisBlobDescriptorCacheProvider returns a new redis-based
return &base{&redisLayerInfoCache{ // BlobDescriptorCacheProvider using the provided redis connection pool.
func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) BlobDescriptorCacheProvider {
return &redisBlobDescriptorService{
pool: pool, pool: pool,
}} }
} }
// Contains does a membership check on the repository blob set in redis. This // RepositoryScoped returns the scoped cache.
// is used as an access check before looking up global path information. If func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
// false is returned, the caller should still check the backend to if it if err := v2.ValidateRespositoryName(repo); err != nil {
// exists elsewhere. return nil, err
func (rlic *redisLayerInfoCache) Contains(ctx context.Context, repo string, dgst digest.Digest) (bool, error) { }
conn := rlic.pool.Get()
defer conn.Close()
ctxu.GetLogger(ctx).Debugf("(*redisLayerInfoCache).Contains(%q, %q)", repo, dgst) return &repositoryScopedRedisBlobDescriptorService{
return redis.Bool(conn.Do("SISMEMBER", rlic.repositoryBlobSetKey(repo), dgst)) repo: repo,
upstream: rbds,
}, nil
} }
// Add adds the layer to the redis repository blob set. // Stat retrieves the descriptor data from the redis hash entry.
func (rlic *redisLayerInfoCache) Add(ctx context.Context, repo string, dgst digest.Digest) error { func (rbds *redisBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
conn := rlic.pool.Get() if err := validateDigest(dgst); err != nil {
return distribution.Descriptor{}, err
}
conn := rbds.pool.Get()
defer conn.Close() defer conn.Close()
ctxu.GetLogger(ctx).Debugf("(*redisLayerInfoCache).Add(%q, %q)", repo, dgst) return rbds.stat(ctx, conn, dgst)
_, err := conn.Do("SADD", rlic.repositoryBlobSetKey(repo), dgst)
return err
} }
// Meta retrieves the layer meta data from the redis hash, returning // stat provides an internal stat call that takes a connection parameter. This
// ErrUnknownLayer if not found. // allows some internal management of the connection scope.
func (rlic *redisLayerInfoCache) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) { func (rbds *redisBlobDescriptorService) stat(ctx context.Context, conn redis.Conn, dgst digest.Digest) (distribution.Descriptor, error) {
conn := rlic.pool.Get() reply, err := redis.Values(conn.Do("HMGET", rbds.blobDescriptorHashKey(dgst), "digest", "length", "mediatype"))
defer conn.Close()
reply, err := redis.Values(conn.Do("HMGET", rlic.blobMetaHashKey(dgst), "path", "length"))
if err != nil { if err != nil {
return LayerMeta{}, err return distribution.Descriptor{}, err
} }
if len(reply) < 2 || reply[0] == nil || reply[1] == nil { if len(reply) < 2 || reply[0] == nil || reply[1] == nil { // don't care if mediatype is nil
return LayerMeta{}, ErrNotFound return distribution.Descriptor{}, distribution.ErrBlobUnknown
} }
var meta LayerMeta var desc distribution.Descriptor
if _, err := redis.Scan(reply, &meta.Path, &meta.Length); err != nil { if _, err := redis.Scan(reply, &desc.Digest, &desc.Length, &desc.MediaType); err != nil {
return LayerMeta{}, err return distribution.Descriptor{}, err
} }
return meta, nil return desc, nil
} }
// SetMeta sets the meta data for the given digest using a redis hash. A hash // SetDescriptor sets the descriptor data for the given digest using a redis
// is used here since we may store unrelated fields about a layer in the // hash. A hash is used here since we may store unrelated fields about a layer
// future. // in the future.
func (rlic *redisLayerInfoCache) SetMeta(ctx context.Context, dgst digest.Digest, meta LayerMeta) error { func (rbds *redisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
conn := rlic.pool.Get() if err := validateDigest(dgst); err != nil {
return err
}
if err := validateDescriptor(desc); err != nil {
return err
}
conn := rbds.pool.Get()
defer conn.Close() defer conn.Close()
_, err := conn.Do("HMSET", rlic.blobMetaHashKey(dgst), "path", meta.Path, "length", meta.Length) return rbds.setDescriptor(ctx, conn, dgst, desc)
return err
} }
// repositoryBlobSetKey returns the key for the blob set in the cache. func (rbds *redisBlobDescriptorService) setDescriptor(ctx context.Context, conn redis.Conn, dgst digest.Digest, desc distribution.Descriptor) error {
func (rlic *redisLayerInfoCache) repositoryBlobSetKey(repo string) string { if _, err := conn.Do("HMSET", rbds.blobDescriptorHashKey(dgst),
return "repository::" + repo + "::blobs" "digest", desc.Digest,
"length", desc.Length); err != nil {
return err
}
// Only set mediatype if not already set.
if _, err := conn.Do("HSETNX", rbds.blobDescriptorHashKey(dgst),
"mediatype", desc.MediaType); err != nil {
return err
}
return nil
} }
// blobMetaHashKey returns the cache key for immutable blob meta data. func (rbds *redisBlobDescriptorService) blobDescriptorHashKey(dgst digest.Digest) string {
func (rlic *redisLayerInfoCache) blobMetaHashKey(dgst digest.Digest) string {
return "blobs::" + dgst.String() return "blobs::" + dgst.String()
} }
type repositoryScopedRedisBlobDescriptorService struct {
repo string
upstream *redisBlobDescriptorService
}
var _ distribution.BlobDescriptorService = &repositoryScopedRedisBlobDescriptorService{}
// Stat ensures that the digest is a member of the specified repository and
// forwards the descriptor request to the global blob store. If the media type
// differs for the repository, we override it.
func (rsrbds *repositoryScopedRedisBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
if err := validateDigest(dgst); err != nil {
return distribution.Descriptor{}, err
}
conn := rsrbds.upstream.pool.Get()
defer conn.Close()
// Check membership to repository first
member, err := redis.Bool(conn.Do("SISMEMBER", rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst))
if err != nil {
return distribution.Descriptor{}, err
}
if !member {
return distribution.Descriptor{}, distribution.ErrBlobUnknown
}
upstream, err := rsrbds.upstream.stat(ctx, conn, dgst)
if err != nil {
return distribution.Descriptor{}, err
}
// We allow a per repository mediatype, let's look it up here.
mediatype, err := redis.String(conn.Do("HGET", rsrbds.blobDescriptorHashKey(dgst), "mediatype"))
if err != nil {
return distribution.Descriptor{}, err
}
if mediatype != "" {
upstream.MediaType = mediatype
}
return upstream, nil
}
func (rsrbds *repositoryScopedRedisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
if err := validateDigest(dgst); err != nil {
return err
}
if err := validateDescriptor(desc); err != nil {
return err
}
if dgst != desc.Digest {
if dgst.Algorithm() == desc.Digest.Algorithm() {
return fmt.Errorf("redis cache: digest for descriptors differ but algorthim does not: %q != %q", dgst, desc.Digest)
}
}
conn := rsrbds.upstream.pool.Get()
defer conn.Close()
return rsrbds.setDescriptor(ctx, conn, dgst, desc)
}
func (rsrbds *repositoryScopedRedisBlobDescriptorService) setDescriptor(ctx context.Context, conn redis.Conn, dgst digest.Digest, desc distribution.Descriptor) error {
if _, err := conn.Do("SADD", rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst); err != nil {
return err
}
if err := rsrbds.upstream.setDescriptor(ctx, conn, dgst, desc); err != nil {
return err
}
// Override repository mediatype.
if _, err := conn.Do("HSET", rsrbds.blobDescriptorHashKey(dgst), "mediatype", desc.MediaType); err != nil {
return err
}
// Also set the values for the primary descriptor, if they differ by
// algorithm (ie sha256 vs tarsum).
if desc.Digest != "" && dgst != desc.Digest && dgst.Algorithm() != desc.Digest.Algorithm() {
if err := rsrbds.setDescriptor(ctx, conn, desc.Digest, desc); err != nil {
return err
}
}
return nil
}
func (rsrbds *repositoryScopedRedisBlobDescriptorService) blobDescriptorHashKey(dgst digest.Digest) string {
return "repository::" + rsrbds.repo + "::blobs::" + dgst.String()
}
func (rsrbds *repositoryScopedRedisBlobDescriptorService) repositoryBlobSetKey(repo string) string {
return "repository::" + rsrbds.repo + "::blobs"
}

View file

@ -17,7 +17,7 @@ func init() {
// TestRedisLayerInfoCache exercises a live redis instance using the cache // TestRedisLayerInfoCache exercises a live redis instance using the cache
// implementation. // implementation.
func TestRedisLayerInfoCache(t *testing.T) { func TestRedisBlobDescriptorCacheProvider(t *testing.T) {
if redisAddr == "" { if redisAddr == "" {
// fallback to an environement variable // fallback to an environement variable
redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR") redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR")
@ -46,5 +46,5 @@ func TestRedisLayerInfoCache(t *testing.T) {
t.Fatalf("unexpected error flushing redis db: %v", err) t.Fatalf("unexpected error flushing redis db: %v", err)
} }
checkLayerInfoCache(t, NewRedisLayerInfoCache(pool)) checkBlobDescriptorCache(t, NewRedisBlobDescriptorCacheProvider(pool))
} }

View file

@ -0,0 +1,84 @@
package storage
import (
"expvar"
"sync/atomic"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution"
)
type cachedBlobStatter struct {
cache distribution.BlobDescriptorService
backend distribution.BlobStatter
}
func (cbds *cachedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
atomic.AddUint64(&blobStatterCacheMetrics.Stat.Requests, 1)
desc, err := cbds.cache.Stat(ctx, dgst)
if err != nil {
if err != distribution.ErrBlobUnknown {
context.GetLogger(ctx).Errorf("error retrieving descriptor from cache: %v", err)
}
goto fallback
}
atomic.AddUint64(&blobStatterCacheMetrics.Stat.Hits, 1)
return desc, nil
fallback:
atomic.AddUint64(&blobStatterCacheMetrics.Stat.Misses, 1)
desc, err = cbds.backend.Stat(ctx, dgst)
if err != nil {
return desc, err
}
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil {
context.GetLogger(ctx).Errorf("error adding descriptor %v to cache: %v", desc.Digest, err)
}
return desc, err
}
// blobStatterCacheMetrics keeps track of cache metrics for blob descriptor
// cache requests. Note this is kept globally and made available via expvar.
// For more detailed metrics, its recommend to instrument a particular cache
// implementation.
var blobStatterCacheMetrics struct {
// Stat tracks calls to the caches.
Stat struct {
Requests uint64
Hits uint64
Misses uint64
}
}
func init() {
registry := expvar.Get("registry")
if registry == nil {
registry = expvar.NewMap("registry")
}
cache := registry.(*expvar.Map).Get("cache")
if cache == nil {
cache = &expvar.Map{}
cache.(*expvar.Map).Init()
registry.(*expvar.Map).Set("cache", cache)
}
storage := cache.(*expvar.Map).Get("storage")
if storage == nil {
storage = &expvar.Map{}
storage.(*expvar.Map).Init()
cache.(*expvar.Map).Set("storage", storage)
}
storage.(*expvar.Map).Set("blobdescriptor", expvar.Func(func() interface{} {
// no need for synchronous access: the increments are atomic and
// during reading, we don't care if the data is up to date. The
// numbers will always *eventually* be reported correctly.
return blobStatterCacheMetrics
}))
}

View file

@ -7,7 +7,6 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"time"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
storagedriver "github.com/docker/distribution/registry/storage/driver" storagedriver "github.com/docker/distribution/registry/storage/driver"
@ -29,9 +28,8 @@ type fileReader struct {
ctx context.Context ctx context.Context
// identifying fields // identifying fields
path string path string
size int64 // size is the total size, must be set. size int64 // size is the total size, must be set.
modtime time.Time // TODO(stevvooe): This is not needed anymore.
// mutable fields // mutable fields
rc io.ReadCloser // remote read closer rc io.ReadCloser // remote read closer
@ -40,41 +38,17 @@ type fileReader struct {
err error // terminal error, if set, reader is closed err error // terminal error, if set, reader is closed
} }
// newFileReader initializes a file reader for the remote file. The read takes // newFileReader initializes a file reader for the remote file. The reader
// on the offset and size at the time the reader is created. If the underlying // takes on the size and path that must be determined externally with a stat
// file changes, one must create a new fileReader. // call. The reader operates optimistically, assuming that the file is already
func newFileReader(ctx context.Context, driver storagedriver.StorageDriver, path string) (*fileReader, error) { // there.
rd := &fileReader{ func newFileReader(ctx context.Context, driver storagedriver.StorageDriver, path string, size int64) (*fileReader, error) {
return &fileReader{
ctx: ctx,
driver: driver, driver: driver,
path: path, path: path,
ctx: ctx, size: size,
} }, nil
// Grab the size of the layer file, ensuring existence.
if fi, err := driver.Stat(ctx, path); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
// NOTE(stevvooe): We really don't care if the file is not
// actually present for the reader. If the caller needs to know
// whether or not the file exists, they should issue a stat call
// on the path. There is still no guarantee, since the file may be
// gone by the time the reader is created. The only correct
// behavior is to return a reader that immediately returns EOF.
default:
// Any other error we want propagated up the stack.
return nil, err
}
} else {
if fi.IsDir() {
return nil, fmt.Errorf("cannot read a directory")
}
// Fill in file information
rd.size = fi.Size()
rd.modtime = fi.ModTime()
}
return rd, nil
} }
func (fr *fileReader) Read(p []byte) (n int, err error) { func (fr *fileReader) Read(p []byte) (n int, err error) {
@ -162,11 +136,6 @@ func (fr *fileReader) reader() (io.Reader, error) {
fr.rc = rc fr.rc = rc
if fr.brd == nil { if fr.brd == nil {
// TODO(stevvooe): Set an optimal buffer size here. We'll have to
// understand the latency characteristics of the underlying network to
// set this correctly, so we may want to leave it to the driver. For
// out of process drivers, we'll have to optimize this buffer size for
// local communication.
fr.brd = bufio.NewReaderSize(fr.rc, fileReaderBufferSize) fr.brd = bufio.NewReaderSize(fr.rc, fileReaderBufferSize)
} else { } else {
fr.brd.Reset(fr.rc) fr.brd.Reset(fr.rc)

View file

@ -37,7 +37,7 @@ func TestSimpleRead(t *testing.T) {
t.Fatalf("error putting patterned content: %v", err) t.Fatalf("error putting patterned content: %v", err)
} }
fr, err := newFileReader(ctx, driver, path) fr, err := newFileReader(ctx, driver, path, int64(len(content)))
if err != nil { if err != nil {
t.Fatalf("error allocating file reader: %v", err) t.Fatalf("error allocating file reader: %v", err)
} }
@ -66,7 +66,7 @@ func TestFileReaderSeek(t *testing.T) {
t.Fatalf("error putting patterned content: %v", err) t.Fatalf("error putting patterned content: %v", err)
} }
fr, err := newFileReader(ctx, driver, path) fr, err := newFileReader(ctx, driver, path, int64(len(content)))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating file reader: %v", err) t.Fatalf("unexpected error creating file reader: %v", err)
@ -162,7 +162,7 @@ func TestFileReaderSeek(t *testing.T) {
// read method, with an io.EOF error. // read method, with an io.EOF error.
func TestFileReaderNonExistentFile(t *testing.T) { func TestFileReaderNonExistentFile(t *testing.T) {
driver := inmemory.New() driver := inmemory.New()
fr, err := newFileReader(context.Background(), driver, "/doesnotexist") fr, err := newFileReader(context.Background(), driver, "/doesnotexist", 10)
if err != nil { if err != nil {
t.Fatalf("unexpected error initializing reader: %v", err) t.Fatalf("unexpected error initializing reader: %v", err)
} }

View file

@ -39,7 +39,6 @@ type bufferedFileWriter struct {
// filewriter should implement. // filewriter should implement.
type fileWriterInterface interface { type fileWriterInterface interface {
io.WriteSeeker io.WriteSeeker
io.WriterAt
io.ReaderFrom io.ReaderFrom
io.Closer io.Closer
} }
@ -110,21 +109,31 @@ func (bfw *bufferedFileWriter) Flush() error {
// Write writes the buffer p at the current write offset. // Write writes the buffer p at the current write offset.
func (fw *fileWriter) Write(p []byte) (n int, err error) { func (fw *fileWriter) Write(p []byte) (n int, err error) {
nn, err := fw.readFromAt(bytes.NewReader(p), -1) nn, err := fw.ReadFrom(bytes.NewReader(p))
return int(nn), err
}
// WriteAt writes p at the specified offset. The underlying offset does not
// change.
func (fw *fileWriter) WriteAt(p []byte, offset int64) (n int, err error) {
nn, err := fw.readFromAt(bytes.NewReader(p), offset)
return int(nn), err return int(nn), err
} }
// ReadFrom reads reader r until io.EOF writing the contents at the current // ReadFrom reads reader r until io.EOF writing the contents at the current
// offset. // offset.
func (fw *fileWriter) ReadFrom(r io.Reader) (n int64, err error) { func (fw *fileWriter) ReadFrom(r io.Reader) (n int64, err error) {
return fw.readFromAt(r, -1) if fw.err != nil {
return 0, fw.err
}
nn, err := fw.driver.WriteStream(fw.ctx, fw.path, fw.offset, r)
// We should forward the offset, whether or not there was an error.
// Basically, we keep the filewriter in sync with the reader's head. If an
// error is encountered, the whole thing should be retried but we proceed
// from an expected offset, even if the data didn't make it to the
// backend.
fw.offset += nn
if fw.offset > fw.size {
fw.size = fw.offset
}
return nn, err
} }
// Seek moves the write position do the requested offest based on the whence // Seek moves the write position do the requested offest based on the whence
@ -169,34 +178,3 @@ func (fw *fileWriter) Close() error {
return nil return nil
} }
// readFromAt writes to fw from r at the specified offset. If offset is less
// than zero, the value of fw.offset is used and updated after the operation.
func (fw *fileWriter) readFromAt(r io.Reader, offset int64) (n int64, err error) {
if fw.err != nil {
return 0, fw.err
}
var updateOffset bool
if offset < 0 {
offset = fw.offset
updateOffset = true
}
nn, err := fw.driver.WriteStream(fw.ctx, fw.path, offset, r)
if updateOffset {
// We should forward the offset, whether or not there was an error.
// Basically, we keep the filewriter in sync with the reader's head. If an
// error is encountered, the whole thing should be retried but we proceed
// from an expected offset, even if the data didn't make it to the
// backend.
fw.offset += nn
if fw.offset > fw.size {
fw.size = fw.offset
}
}
return nn, err
}

View file

@ -51,7 +51,7 @@ func TestSimpleWrite(t *testing.T) {
t.Fatalf("unexpected write length: %d != %d", n, len(content)) t.Fatalf("unexpected write length: %d != %d", n, len(content))
} }
fr, err := newFileReader(ctx, driver, path) fr, err := newFileReader(ctx, driver, path, int64(len(content)))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating fileReader: %v", err) t.Fatalf("unexpected error creating fileReader: %v", err)
} }
@ -78,23 +78,23 @@ func TestSimpleWrite(t *testing.T) {
t.Fatalf("write did not advance offset: %d != %d", end, len(content)) t.Fatalf("write did not advance offset: %d != %d", end, len(content))
} }
// Double the content, but use the WriteAt method // Double the content
doubled := append(content, content...) doubled := append(content, content...)
doubledgst, err := digest.FromReader(bytes.NewReader(doubled)) doubledgst, err := digest.FromReader(bytes.NewReader(doubled))
if err != nil { if err != nil {
t.Fatalf("unexpected error digesting doubled content: %v", err) t.Fatalf("unexpected error digesting doubled content: %v", err)
} }
n, err = fw.WriteAt(content, end) nn, err := fw.ReadFrom(bytes.NewReader(content))
if err != nil { if err != nil {
t.Fatalf("unexpected error writing content at %d: %v", end, err) t.Fatalf("unexpected error doubling content: %v", err)
} }
if n != len(content) { if nn != int64(len(content)) {
t.Fatalf("writeat was short: %d != %d", n, len(content)) t.Fatalf("writeat was short: %d != %d", n, len(content))
} }
fr, err = newFileReader(ctx, driver, path) fr, err = newFileReader(ctx, driver, path, int64(len(doubled)))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating fileReader: %v", err) t.Fatalf("unexpected error creating fileReader: %v", err)
} }
@ -111,20 +111,20 @@ func TestSimpleWrite(t *testing.T) {
t.Fatalf("unable to verify write data") t.Fatalf("unable to verify write data")
} }
// Check that WriteAt didn't update the offset. // Check that Write updated the offset.
end, err = fw.Seek(0, os.SEEK_END) end, err = fw.Seek(0, os.SEEK_END)
if err != nil { if err != nil {
t.Fatalf("unexpected error seeking: %v", err) t.Fatalf("unexpected error seeking: %v", err)
} }
if end != int64(len(content)) { if end != int64(len(doubled)) {
t.Fatalf("write did not advance offset: %d != %d", end, len(content)) t.Fatalf("write did not advance offset: %d != %d", end, len(doubled))
} }
// Now, we copy from one path to another, running the data through the // Now, we copy from one path to another, running the data through the
// fileReader to fileWriter, rather than the driver.Move command to ensure // fileReader to fileWriter, rather than the driver.Move command to ensure
// everything is working correctly. // everything is working correctly.
fr, err = newFileReader(ctx, driver, path) fr, err = newFileReader(ctx, driver, path, int64(len(doubled)))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating fileReader: %v", err) t.Fatalf("unexpected error creating fileReader: %v", err)
} }
@ -136,7 +136,7 @@ func TestSimpleWrite(t *testing.T) {
} }
defer fw.Close() defer fw.Close()
nn, err := io.Copy(fw, fr) nn, err = io.Copy(fw, fr)
if err != nil { if err != nil {
t.Fatalf("unexpected error copying data: %v", err) t.Fatalf("unexpected error copying data: %v", err)
} }
@ -145,7 +145,7 @@ func TestSimpleWrite(t *testing.T) {
t.Fatalf("unexpected copy length: %d != %d", nn, len(doubled)) t.Fatalf("unexpected copy length: %d != %d", nn, len(doubled))
} }
fr, err = newFileReader(ctx, driver, "/copied") fr, err = newFileReader(ctx, driver, "/copied", int64(len(doubled)))
if err != nil { if err != nil {
t.Fatalf("unexpected error creating fileReader: %v", err) t.Fatalf("unexpected error creating fileReader: %v", err)
} }

View file

@ -1,202 +0,0 @@
package storage
import (
"expvar"
"sync/atomic"
"time"
"github.com/docker/distribution"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/driver"
"golang.org/x/net/context"
)
// cachedLayerService implements the layer service with path-aware caching,
// using a LayerInfoCache interface.
type cachedLayerService struct {
distribution.LayerService // upstream layer service
repository distribution.Repository
ctx context.Context
driver driver.StorageDriver
*blobStore // global blob store
cache cache.LayerInfoCache
}
// Exists checks for existence of the digest in the cache, immediately
// returning if it exists for the repository. If not, the upstream is checked.
// When a positive result is found, it is written into the cache.
func (lc *cachedLayerService) Exists(dgst digest.Digest) (bool, error) {
ctxu.GetLogger(lc.ctx).Debugf("(*cachedLayerService).Exists(%q)", dgst)
now := time.Now()
defer func() {
// TODO(stevvooe): Replace this with a decent context-based metrics solution
ctxu.GetLoggerWithField(lc.ctx, "blob.exists.duration", time.Since(now)).
Infof("(*cachedLayerService).Exists(%q)", dgst)
}()
atomic.AddUint64(&layerInfoCacheMetrics.Exists.Requests, 1)
available, err := lc.cache.Contains(lc.ctx, lc.repository.Name(), dgst)
if err != nil {
ctxu.GetLogger(lc.ctx).Errorf("error checking availability of %v@%v: %v", lc.repository.Name(), dgst, err)
goto fallback
}
if available {
atomic.AddUint64(&layerInfoCacheMetrics.Exists.Hits, 1)
return true, nil
}
fallback:
atomic.AddUint64(&layerInfoCacheMetrics.Exists.Misses, 1)
exists, err := lc.LayerService.Exists(dgst)
if err != nil {
return exists, err
}
if exists {
// we can only cache this if the existence is positive.
if err := lc.cache.Add(lc.ctx, lc.repository.Name(), dgst); err != nil {
ctxu.GetLogger(lc.ctx).Errorf("error adding %v@%v to cache: %v", lc.repository.Name(), dgst, err)
}
}
return exists, err
}
// Fetch checks for the availability of the layer in the repository via the
// cache. If present, the metadata is resolved and the layer is returned. If
// any operation fails, the layer is read directly from the upstream. The
// results are cached, if possible.
func (lc *cachedLayerService) Fetch(dgst digest.Digest) (distribution.Layer, error) {
ctxu.GetLogger(lc.ctx).Debugf("(*layerInfoCache).Fetch(%q)", dgst)
now := time.Now()
defer func() {
ctxu.GetLoggerWithField(lc.ctx, "blob.fetch.duration", time.Since(now)).
Infof("(*layerInfoCache).Fetch(%q)", dgst)
}()
atomic.AddUint64(&layerInfoCacheMetrics.Fetch.Requests, 1)
available, err := lc.cache.Contains(lc.ctx, lc.repository.Name(), dgst)
if err != nil {
ctxu.GetLogger(lc.ctx).Errorf("error checking availability of %v@%v: %v", lc.repository.Name(), dgst, err)
goto fallback
}
if available {
// fast path: get the layer info and return
meta, err := lc.cache.Meta(lc.ctx, dgst)
if err != nil {
ctxu.GetLogger(lc.ctx).Errorf("error fetching %v@%v from cache: %v", lc.repository.Name(), dgst, err)
goto fallback
}
atomic.AddUint64(&layerInfoCacheMetrics.Fetch.Hits, 1)
return newLayerReader(lc.driver, dgst, meta.Path, meta.Length)
}
// NOTE(stevvooe): Unfortunately, the cache here only makes checks for
// existing layers faster. We'd have to provide more careful
// synchronization with the backend to make the missing case as fast.
fallback:
atomic.AddUint64(&layerInfoCacheMetrics.Fetch.Misses, 1)
layer, err := lc.LayerService.Fetch(dgst)
if err != nil {
return nil, err
}
// add the layer to the repository
if err := lc.cache.Add(lc.ctx, lc.repository.Name(), dgst); err != nil {
ctxu.GetLogger(lc.ctx).
Errorf("error caching repository relationship for %v@%v: %v", lc.repository.Name(), dgst, err)
}
// lookup layer path and add it to the cache, if it succeds. Note that we
// still return the layer even if we have trouble caching it.
if path, err := lc.resolveLayerPath(layer); err != nil {
ctxu.GetLogger(lc.ctx).
Errorf("error resolving path while caching %v@%v: %v", lc.repository.Name(), dgst, err)
} else {
// add the layer to the cache once we've resolved the path.
if err := lc.cache.SetMeta(lc.ctx, dgst, cache.LayerMeta{Path: path, Length: layer.Length()}); err != nil {
ctxu.GetLogger(lc.ctx).Errorf("error adding meta for %v@%v to cache: %v", lc.repository.Name(), dgst, err)
}
}
return layer, err
}
// extractLayerInfo pulls the layerInfo from the layer, attempting to get the
// path information from either the concrete object or by resolving the
// primary blob store path.
func (lc *cachedLayerService) resolveLayerPath(layer distribution.Layer) (path string, err error) {
// try and resolve the type and driver, so we don't have to traverse links
switch v := layer.(type) {
case *layerReader:
// only set path if we have same driver instance.
if v.driver == lc.driver {
return v.path, nil
}
}
ctxu.GetLogger(lc.ctx).Warnf("resolving layer path during cache lookup (%v@%v)", lc.repository.Name(), layer.Digest())
// we have to do an expensive stat to resolve the layer location but no
// need to check the link, since we already have layer instance for this
// repository.
bp, err := lc.blobStore.path(layer.Digest())
if err != nil {
return "", err
}
return bp, nil
}
// layerInfoCacheMetrics keeps track of cache metrics for layer info cache
// requests. Note this is kept globally and made available via expvar. For
// more detailed metrics, its recommend to instrument a particular cache
// implementation.
var layerInfoCacheMetrics struct {
// Exists tracks calls to the Exists caches.
Exists struct {
Requests uint64
Hits uint64
Misses uint64
}
// Fetch tracks calls to the fetch caches.
Fetch struct {
Requests uint64
Hits uint64
Misses uint64
}
}
func init() {
registry := expvar.Get("registry")
if registry == nil {
registry = expvar.NewMap("registry")
}
cache := registry.(*expvar.Map).Get("cache")
if cache == nil {
cache = &expvar.Map{}
cache.(*expvar.Map).Init()
registry.(*expvar.Map).Set("cache", cache)
}
storage := cache.(*expvar.Map).Get("storage")
if storage == nil {
storage = &expvar.Map{}
storage.(*expvar.Map).Init()
cache.(*expvar.Map).Set("storage", storage)
}
storage.(*expvar.Map).Set("layerinfo", expvar.Func(func() interface{} {
// no need for synchronous access: the increments are atomic and
// during reading, we don't care if the data is up to date. The
// numbers will always *eventually* be reported correctly.
return layerInfoCacheMetrics
}))
}

View file

@ -1,104 +0,0 @@
package storage
import (
"fmt"
"net/http"
"time"
"github.com/docker/distribution"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/driver"
)
// layerReader implements Layer and provides facilities for reading and
// seeking.
type layerReader struct {
fileReader
digest digest.Digest
}
// newLayerReader returns a new layerReader with the digest, path and length,
// eliding round trips to the storage backend.
func newLayerReader(driver driver.StorageDriver, dgst digest.Digest, path string, length int64) (*layerReader, error) {
fr := &fileReader{
driver: driver,
path: path,
size: length,
}
return &layerReader{
fileReader: *fr,
digest: dgst,
}, nil
}
var _ distribution.Layer = &layerReader{}
func (lr *layerReader) Digest() digest.Digest {
return lr.digest
}
func (lr *layerReader) Length() int64 {
return lr.size
}
func (lr *layerReader) CreatedAt() time.Time {
return lr.modtime
}
// Close the layer. Should be called when the resource is no longer needed.
func (lr *layerReader) Close() error {
return lr.closeWithErr(distribution.ErrLayerClosed)
}
func (lr *layerReader) Handler(r *http.Request) (h http.Handler, err error) {
var handlerFunc http.HandlerFunc
redirectURL, err := lr.fileReader.driver.URLFor(lr.ctx, lr.path, map[string]interface{}{"method": r.Method})
switch err {
case nil:
handlerFunc = func(w http.ResponseWriter, r *http.Request) {
// Redirect to storage URL.
http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect)
}
case driver.ErrUnsupportedMethod:
handlerFunc = func(w http.ResponseWriter, r *http.Request) {
// Fallback to serving the content directly.
http.ServeContent(w, r, lr.digest.String(), lr.CreatedAt(), lr)
}
default:
// Some unexpected error.
return nil, err
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If the registry is serving this content itself, check
// the If-None-Match header and return 304 on match. Redirected
// storage implementations do the same.
if etagMatch(r, lr.digest.String()) {
w.WriteHeader(http.StatusNotModified)
return
}
setCacheHeaders(w, 86400, lr.digest.String())
w.Header().Set("Docker-Content-Digest", lr.digest.String())
handlerFunc.ServeHTTP(w, r)
}), nil
}
func etagMatch(r *http.Request, etag string) bool {
for _, headerVal := range r.Header["If-None-Match"] {
if headerVal == etag {
return true
}
}
return false
}
func setCacheHeaders(w http.ResponseWriter, cacheAge int, etag string) {
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", cacheAge))
}

View file

@ -1,178 +0,0 @@
package storage
import (
"time"
"code.google.com/p/go-uuid/uuid"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest"
storagedriver "github.com/docker/distribution/registry/storage/driver"
)
type layerStore struct {
repository *repository
}
func (ls *layerStore) Exists(digest digest.Digest) (bool, error) {
context.GetLogger(ls.repository.ctx).Debug("(*layerStore).Exists")
// Because this implementation just follows blob links, an existence check
// is pretty cheap by starting and closing a fetch.
_, err := ls.Fetch(digest)
if err != nil {
switch err.(type) {
case distribution.ErrUnknownLayer:
return false, nil
}
return false, err
}
return true, nil
}
func (ls *layerStore) Fetch(dgst digest.Digest) (distribution.Layer, error) {
ctx := ls.repository.ctx
context.GetLogger(ctx).Debug("(*layerStore).Fetch")
bp, err := ls.path(dgst)
if err != nil {
return nil, err
}
fr, err := newFileReader(ctx, ls.repository.driver, bp)
if err != nil {
return nil, err
}
return &layerReader{
fileReader: *fr,
digest: dgst,
}, nil
}
// Upload begins a layer upload, returning a handle. If the layer upload
// is already in progress or the layer has already been uploaded, this
// will return an error.
func (ls *layerStore) Upload() (distribution.LayerUpload, error) {
ctx := ls.repository.ctx
context.GetLogger(ctx).Debug("(*layerStore).Upload")
// NOTE(stevvooe): Consider the issues with allowing concurrent upload of
// the same two layers. Should it be disallowed? For now, we allow both
// parties to proceed and the the first one uploads the layer.
uuid := uuid.New()
startedAt := time.Now().UTC()
path, err := ls.repository.pm.path(uploadDataPathSpec{
name: ls.repository.Name(),
uuid: uuid,
})
if err != nil {
return nil, err
}
startedAtPath, err := ls.repository.pm.path(uploadStartedAtPathSpec{
name: ls.repository.Name(),
uuid: uuid,
})
if err != nil {
return nil, err
}
// Write a startedat file for this upload
if err := ls.repository.driver.PutContent(ctx, startedAtPath, []byte(startedAt.Format(time.RFC3339))); err != nil {
return nil, err
}
return ls.newLayerUpload(uuid, path, startedAt)
}
// Resume continues an in progress layer upload, returning the current
// state of the upload.
func (ls *layerStore) Resume(uuid string) (distribution.LayerUpload, error) {
ctx := ls.repository.ctx
context.GetLogger(ctx).Debug("(*layerStore).Resume")
startedAtPath, err := ls.repository.pm.path(uploadStartedAtPathSpec{
name: ls.repository.Name(),
uuid: uuid,
})
if err != nil {
return nil, err
}
startedAtBytes, err := ls.repository.driver.GetContent(ctx, startedAtPath)
if err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
return nil, distribution.ErrLayerUploadUnknown
default:
return nil, err
}
}
startedAt, err := time.Parse(time.RFC3339, string(startedAtBytes))
if err != nil {
return nil, err
}
path, err := ls.repository.pm.path(uploadDataPathSpec{
name: ls.repository.Name(),
uuid: uuid,
})
if err != nil {
return nil, err
}
return ls.newLayerUpload(uuid, path, startedAt)
}
// newLayerUpload allocates a new upload controller with the given state.
func (ls *layerStore) newLayerUpload(uuid, path string, startedAt time.Time) (distribution.LayerUpload, error) {
fw, err := newFileWriter(ls.repository.ctx, ls.repository.driver, path)
if err != nil {
return nil, err
}
lw := &layerWriter{
layerStore: ls,
uuid: uuid,
startedAt: startedAt,
bufferedFileWriter: *fw,
}
lw.setupResumableDigester()
return lw, nil
}
func (ls *layerStore) path(dgst digest.Digest) (string, error) {
// We must traverse this path through the link to enforce ownership.
layerLinkPath, err := ls.repository.pm.path(layerLinkPathSpec{name: ls.repository.Name(), digest: dgst})
if err != nil {
return "", err
}
blobPath, err := ls.repository.blobStore.resolve(layerLinkPath)
if err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
return "", distribution.ErrUnknownLayer{
FSLayer: manifest.FSLayer{BlobSum: dgst},
}
default:
return "", err
}
}
return blobPath, nil
}

View file

@ -1,478 +0,0 @@
package storage
import (
"fmt"
"io"
"os"
"path"
"strconv"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
storagedriver "github.com/docker/distribution/registry/storage/driver"
)
var _ distribution.LayerUpload = &layerWriter{}
// layerWriter is used to control the various aspects of resumable
// layer upload. It implements the LayerUpload interface.
type layerWriter struct {
layerStore *layerStore
uuid string
startedAt time.Time
resumableDigester digest.ResumableDigester
// implementes io.WriteSeeker, io.ReaderFrom and io.Closer to satisfy
// LayerUpload Interface
bufferedFileWriter
}
var _ distribution.LayerUpload = &layerWriter{}
// UUID returns the identifier for this upload.
func (lw *layerWriter) UUID() string {
return lw.uuid
}
func (lw *layerWriter) StartedAt() time.Time {
return lw.startedAt
}
// Finish marks the upload as completed, returning a valid handle to the
// uploaded layer. The final size and checksum are validated against the
// contents of the uploaded layer. The checksum should be provided in the
// format <algorithm>:<hex digest>.
func (lw *layerWriter) Finish(dgst digest.Digest) (distribution.Layer, error) {
context.GetLogger(lw.layerStore.repository.ctx).Debug("(*layerWriter).Finish")
if err := lw.bufferedFileWriter.Close(); err != nil {
return nil, err
}
var (
canonical digest.Digest
err error
)
// HACK(stevvooe): To deal with s3's lack of consistency, attempt to retry
// validation on failure. Three attempts are made, backing off
// retries*100ms each time.
for retries := 0; ; retries++ {
canonical, err = lw.validateLayer(dgst)
if err == nil {
break
}
context.GetLoggerWithField(lw.layerStore.repository.ctx, "retries", retries).
Errorf("error validating layer: %v", err)
if retries < 3 {
time.Sleep(100 * time.Millisecond * time.Duration(retries+1))
continue
}
return nil, err
}
if err := lw.moveLayer(canonical); err != nil {
// TODO(stevvooe): Cleanup?
return nil, err
}
// Link the layer blob into the repository.
if err := lw.linkLayer(canonical, dgst); err != nil {
return nil, err
}
if err := lw.removeResources(); err != nil {
return nil, err
}
return lw.layerStore.Fetch(canonical)
}
// Cancel the layer upload process.
func (lw *layerWriter) Cancel() error {
context.GetLogger(lw.layerStore.repository.ctx).Debug("(*layerWriter).Cancel")
if err := lw.removeResources(); err != nil {
return err
}
lw.Close()
return nil
}
func (lw *layerWriter) Write(p []byte) (int, error) {
if lw.resumableDigester == nil {
return lw.bufferedFileWriter.Write(p)
}
// Ensure that the current write offset matches how many bytes have been
// written to the digester. If not, we need to update the digest state to
// match the current write position.
if err := lw.resumeHashAt(lw.offset); err != nil {
return 0, err
}
return io.MultiWriter(&lw.bufferedFileWriter, lw.resumableDigester).Write(p)
}
func (lw *layerWriter) ReadFrom(r io.Reader) (n int64, err error) {
if lw.resumableDigester == nil {
return lw.bufferedFileWriter.ReadFrom(r)
}
// Ensure that the current write offset matches how many bytes have been
// written to the digester. If not, we need to update the digest state to
// match the current write position.
if err := lw.resumeHashAt(lw.offset); err != nil {
return 0, err
}
return lw.bufferedFileWriter.ReadFrom(io.TeeReader(r, lw.resumableDigester))
}
func (lw *layerWriter) Close() error {
if lw.err != nil {
return lw.err
}
if lw.resumableDigester != nil {
if err := lw.storeHashState(); err != nil {
return err
}
}
return lw.bufferedFileWriter.Close()
}
type hashStateEntry struct {
offset int64
path string
}
// getStoredHashStates returns a slice of hashStateEntries for this upload.
func (lw *layerWriter) getStoredHashStates() ([]hashStateEntry, error) {
uploadHashStatePathPrefix, err := lw.layerStore.repository.pm.path(uploadHashStatePathSpec{
name: lw.layerStore.repository.Name(),
uuid: lw.uuid,
alg: lw.resumableDigester.Digest().Algorithm(),
list: true,
})
if err != nil {
return nil, err
}
paths, err := lw.driver.List(lw.layerStore.repository.ctx, uploadHashStatePathPrefix)
if err != nil {
if _, ok := err.(storagedriver.PathNotFoundError); !ok {
return nil, err
}
// Treat PathNotFoundError as no entries.
paths = nil
}
hashStateEntries := make([]hashStateEntry, 0, len(paths))
for _, p := range paths {
pathSuffix := path.Base(p)
// The suffix should be the offset.
offset, err := strconv.ParseInt(pathSuffix, 0, 64)
if err != nil {
logrus.Errorf("unable to parse offset from upload state path %q: %s", p, err)
}
hashStateEntries = append(hashStateEntries, hashStateEntry{offset: offset, path: p})
}
return hashStateEntries, nil
}
// resumeHashAt attempts to restore the state of the internal hash function
// by loading the most recent saved hash state less than or equal to the given
// offset. Any unhashed bytes remaining less than the given offset are hashed
// from the content uploaded so far.
func (lw *layerWriter) resumeHashAt(offset int64) error {
if offset < 0 {
return fmt.Errorf("cannot resume hash at negative offset: %d", offset)
}
if offset == int64(lw.resumableDigester.Len()) {
// State of digester is already at the requested offset.
return nil
}
// List hash states from storage backend.
var hashStateMatch hashStateEntry
hashStates, err := lw.getStoredHashStates()
if err != nil {
return fmt.Errorf("unable to get stored hash states with offset %d: %s", offset, err)
}
ctx := lw.layerStore.repository.ctx
// Find the highest stored hashState with offset less than or equal to
// the requested offset.
for _, hashState := range hashStates {
if hashState.offset == offset {
hashStateMatch = hashState
break // Found an exact offset match.
} else if hashState.offset < offset && hashState.offset > hashStateMatch.offset {
// This offset is closer to the requested offset.
hashStateMatch = hashState
} else if hashState.offset > offset {
// Remove any stored hash state with offsets higher than this one
// as writes to this resumed hasher will make those invalid. This
// is probably okay to skip for now since we don't expect anyone to
// use the API in this way. For that reason, we don't treat an
// an error here as a fatal error, but only log it.
if err := lw.driver.Delete(ctx, hashState.path); err != nil {
logrus.Errorf("unable to delete stale hash state %q: %s", hashState.path, err)
}
}
}
if hashStateMatch.offset == 0 {
// No need to load any state, just reset the hasher.
lw.resumableDigester.Reset()
} else {
storedState, err := lw.driver.GetContent(ctx, hashStateMatch.path)
if err != nil {
return err
}
if err = lw.resumableDigester.Restore(storedState); err != nil {
return err
}
}
// Mind the gap.
if gapLen := offset - int64(lw.resumableDigester.Len()); gapLen > 0 {
// Need to read content from the upload to catch up to the desired offset.
fr, err := newFileReader(ctx, lw.driver, lw.path)
if err != nil {
return err
}
if _, err = fr.Seek(int64(lw.resumableDigester.Len()), os.SEEK_SET); err != nil {
return fmt.Errorf("unable to seek to layer reader offset %d: %s", lw.resumableDigester.Len(), err)
}
if _, err := io.CopyN(lw.resumableDigester, fr, gapLen); err != nil {
return err
}
}
return nil
}
func (lw *layerWriter) storeHashState() error {
uploadHashStatePath, err := lw.layerStore.repository.pm.path(uploadHashStatePathSpec{
name: lw.layerStore.repository.Name(),
uuid: lw.uuid,
alg: lw.resumableDigester.Digest().Algorithm(),
offset: int64(lw.resumableDigester.Len()),
})
if err != nil {
return err
}
hashState, err := lw.resumableDigester.State()
if err != nil {
return err
}
return lw.driver.PutContent(lw.layerStore.repository.ctx, uploadHashStatePath, hashState)
}
// validateLayer checks the layer data against the digest, returning an error
// if it does not match. The canonical digest is returned.
func (lw *layerWriter) validateLayer(dgst digest.Digest) (digest.Digest, error) {
var (
verified, fullHash bool
canonical digest.Digest
)
if lw.resumableDigester != nil {
// Restore the hasher state to the end of the upload.
if err := lw.resumeHashAt(lw.size); err != nil {
return "", err
}
canonical = lw.resumableDigester.Digest()
if canonical.Algorithm() == dgst.Algorithm() {
// Common case: client and server prefer the same canonical digest
// algorithm - currently SHA256.
verified = dgst == canonical
} else {
// The client wants to use a different digest algorithm. They'll just
// have to be patient and wait for us to download and re-hash the
// uploaded content using that digest algorithm.
fullHash = true
}
} else {
// Not using resumable digests, so we need to hash the entire layer.
fullHash = true
}
if fullHash {
digester := digest.NewCanonicalDigester()
digestVerifier, err := digest.NewDigestVerifier(dgst)
if err != nil {
return "", err
}
// Read the file from the backend driver and validate it.
fr, err := newFileReader(lw.layerStore.repository.ctx, lw.bufferedFileWriter.driver, lw.path)
if err != nil {
return "", err
}
tr := io.TeeReader(fr, digester)
if _, err = io.Copy(digestVerifier, tr); err != nil {
return "", err
}
canonical = digester.Digest()
verified = digestVerifier.Verified()
}
if !verified {
context.GetLoggerWithField(lw.layerStore.repository.ctx, "canonical", dgst).
Errorf("canonical digest does match provided digest")
return "", distribution.ErrLayerInvalidDigest{
Digest: dgst,
Reason: fmt.Errorf("content does not match digest"),
}
}
return canonical, nil
}
// moveLayer moves the data into its final, hash-qualified destination,
// identified by dgst. The layer should be validated before commencing the
// move.
func (lw *layerWriter) moveLayer(dgst digest.Digest) error {
blobPath, err := lw.layerStore.repository.pm.path(blobDataPathSpec{
digest: dgst,
})
if err != nil {
return err
}
ctx := lw.layerStore.repository.ctx
// Check for existence
if _, err := lw.driver.Stat(ctx, blobPath); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
break // ensure that it doesn't exist.
default:
return err
}
} else {
// If the path exists, we can assume that the content has already
// been uploaded, since the blob storage is content-addressable.
// While it may be corrupted, detection of such corruption belongs
// elsewhere.
return nil
}
// If no data was received, we may not actually have a file on disk. Check
// the size here and write a zero-length file to blobPath if this is the
// case. For the most part, this should only ever happen with zero-length
// tars.
if _, err := lw.driver.Stat(ctx, lw.path); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
// HACK(stevvooe): This is slightly dangerous: if we verify above,
// get a hash, then the underlying file is deleted, we risk moving
// a zero-length blob into a nonzero-length blob location. To
// prevent this horrid thing, we employ the hack of only allowing
// to this happen for the zero tarsum.
if dgst == digest.DigestSha256EmptyTar {
return lw.driver.PutContent(ctx, blobPath, []byte{})
}
// We let this fail during the move below.
logrus.
WithField("upload.uuid", lw.UUID()).
WithField("digest", dgst).Warnf("attempted to move zero-length content with non-zero digest")
default:
return err // unrelated error
}
}
return lw.driver.Move(ctx, lw.path, blobPath)
}
// linkLayer links a valid, written layer blob into the registry under the
// named repository for the upload controller.
func (lw *layerWriter) linkLayer(canonical digest.Digest, aliases ...digest.Digest) error {
dgsts := append([]digest.Digest{canonical}, aliases...)
// Don't make duplicate links.
seenDigests := make(map[digest.Digest]struct{}, len(dgsts))
for _, dgst := range dgsts {
if _, seen := seenDigests[dgst]; seen {
continue
}
seenDigests[dgst] = struct{}{}
layerLinkPath, err := lw.layerStore.repository.pm.path(layerLinkPathSpec{
name: lw.layerStore.repository.Name(),
digest: dgst,
})
if err != nil {
return err
}
ctx := lw.layerStore.repository.ctx
if err := lw.layerStore.repository.driver.PutContent(ctx, layerLinkPath, []byte(canonical)); err != nil {
return err
}
}
return nil
}
// removeResources should clean up all resources associated with the upload
// instance. An error will be returned if the clean up cannot proceed. If the
// resources are already not present, no error will be returned.
func (lw *layerWriter) removeResources() error {
dataPath, err := lw.layerStore.repository.pm.path(uploadDataPathSpec{
name: lw.layerStore.repository.Name(),
uuid: lw.uuid,
})
if err != nil {
return err
}
// Resolve and delete the containing directory, which should include any
// upload related files.
dirPath := path.Dir(dataPath)
if err := lw.driver.Delete(lw.layerStore.repository.ctx, dirPath); err != nil {
switch err := err.(type) {
case storagedriver.PathNotFoundError:
break // already gone!
default:
// This should be uncommon enough such that returning an error
// should be okay. At this point, the upload should be mostly
// complete, but perhaps the backend became unaccessible.
logrus.Errorf("unable to delete layer upload resources %q: %v", dirPath, err)
return err
}
}
return nil
}

View file

@ -1,6 +0,0 @@
// +build noresumabledigest
package storage
func (lw *layerWriter) setupResumableDigester() {
}

View file

@ -1,9 +0,0 @@
// +build !noresumabledigest
package storage
import "github.com/docker/distribution/digest"
func (lw *layerWriter) setupResumableDigester() {
lw.resumableDigester = digest.NewCanonicalResumableDigester()
}

View file

@ -0,0 +1,258 @@
package storage
import (
"net/http"
"time"
"code.google.com/p/go-uuid/uuid"
"github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/storage/driver"
)
// linkedBlobStore provides a full BlobService that namespaces the blobs to a
// given repository. Effectively, it manages the links in a given repository
// that grant access to the global blob store.
type linkedBlobStore struct {
*blobStore
blobServer distribution.BlobServer
statter distribution.BlobStatter
repository distribution.Repository
ctx context.Context // only to be used where context can't come through method args
// linkPath allows one to control the repository blob link set to which
// the blob store dispatches. This is required because manifest and layer
// blobs have not yet been fully merged. At some point, this functionality
// should be removed an the blob links folder should be merged.
linkPath func(pm *pathMapper, name string, dgst digest.Digest) (string, error)
}
var _ distribution.BlobStore = &linkedBlobStore{}
func (lbs *linkedBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
return lbs.statter.Stat(ctx, dgst)
}
func (lbs *linkedBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
canonical, err := lbs.Stat(ctx, dgst) // access check
if err != nil {
return nil, err
}
return lbs.blobStore.Get(ctx, canonical.Digest)
}
func (lbs *linkedBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
canonical, err := lbs.Stat(ctx, dgst) // access check
if err != nil {
return nil, err
}
return lbs.blobStore.Open(ctx, canonical.Digest)
}
func (lbs *linkedBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
canonical, err := lbs.Stat(ctx, dgst) // access check
if err != nil {
return err
}
if canonical.MediaType != "" {
// Set the repository local content type.
w.Header().Set("Content-Type", canonical.MediaType)
}
return lbs.blobServer.ServeBlob(ctx, w, r, canonical.Digest)
}
func (lbs *linkedBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
// Place the data in the blob store first.
desc, err := lbs.blobStore.Put(ctx, mediaType, p)
if err != nil {
context.GetLogger(ctx).Errorf("error putting into main store: %v", err)
return distribution.Descriptor{}, err
}
// TODO(stevvooe): Write out mediatype if incoming differs from what is
// returned by Put above. Note that we should allow updates for a given
// repository.
return desc, lbs.linkBlob(ctx, desc)
}
// Writer begins a blob write session, returning a handle.
func (lbs *linkedBlobStore) Create(ctx context.Context) (distribution.BlobWriter, error) {
context.GetLogger(ctx).Debug("(*linkedBlobStore).Writer")
uuid := uuid.New()
startedAt := time.Now().UTC()
path, err := lbs.blobStore.pm.path(uploadDataPathSpec{
name: lbs.repository.Name(),
id: uuid,
})
if err != nil {
return nil, err
}
startedAtPath, err := lbs.blobStore.pm.path(uploadStartedAtPathSpec{
name: lbs.repository.Name(),
id: uuid,
})
if err != nil {
return nil, err
}
// Write a startedat file for this upload
if err := lbs.blobStore.driver.PutContent(ctx, startedAtPath, []byte(startedAt.Format(time.RFC3339))); err != nil {
return nil, err
}
return lbs.newBlobUpload(ctx, uuid, path, startedAt)
}
func (lbs *linkedBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
context.GetLogger(ctx).Debug("(*linkedBlobStore).Resume")
startedAtPath, err := lbs.blobStore.pm.path(uploadStartedAtPathSpec{
name: lbs.repository.Name(),
id: id,
})
if err != nil {
return nil, err
}
startedAtBytes, err := lbs.blobStore.driver.GetContent(ctx, startedAtPath)
if err != nil {
switch err := err.(type) {
case driver.PathNotFoundError:
return nil, distribution.ErrBlobUploadUnknown
default:
return nil, err
}
}
startedAt, err := time.Parse(time.RFC3339, string(startedAtBytes))
if err != nil {
return nil, err
}
path, err := lbs.pm.path(uploadDataPathSpec{
name: lbs.repository.Name(),
id: id,
})
if err != nil {
return nil, err
}
return lbs.newBlobUpload(ctx, id, path, startedAt)
}
// newLayerUpload allocates a new upload controller with the given state.
func (lbs *linkedBlobStore) newBlobUpload(ctx context.Context, uuid, path string, startedAt time.Time) (distribution.BlobWriter, error) {
fw, err := newFileWriter(ctx, lbs.driver, path)
if err != nil {
return nil, err
}
bw := &blobWriter{
blobStore: lbs,
id: uuid,
startedAt: startedAt,
bufferedFileWriter: *fw,
}
bw.setupResumableDigester()
return bw, nil
}
// linkBlob links a valid, written blob into the registry under the named
// repository for the upload controller.
func (lbs *linkedBlobStore) linkBlob(ctx context.Context, canonical distribution.Descriptor, aliases ...digest.Digest) error {
dgsts := append([]digest.Digest{canonical.Digest}, aliases...)
// TODO(stevvooe): Need to write out mediatype for only canonical hash
// since we don't care about the aliases. They are generally unused except
// for tarsum but those versions don't care about mediatype.
// Don't make duplicate links.
seenDigests := make(map[digest.Digest]struct{}, len(dgsts))
for _, dgst := range dgsts {
if _, seen := seenDigests[dgst]; seen {
continue
}
seenDigests[dgst] = struct{}{}
blobLinkPath, err := lbs.linkPath(lbs.pm, lbs.repository.Name(), dgst)
if err != nil {
return err
}
if err := lbs.blobStore.link(ctx, blobLinkPath, canonical.Digest); err != nil {
return err
}
}
return nil
}
type linkedBlobStatter struct {
*blobStore
repository distribution.Repository
// linkPath allows one to control the repository blob link set to which
// the blob store dispatches. This is required because manifest and layer
// blobs have not yet been fully merged. At some point, this functionality
// should be removed an the blob links folder should be merged.
linkPath func(pm *pathMapper, name string, dgst digest.Digest) (string, error)
}
var _ distribution.BlobStatter = &linkedBlobStatter{}
func (lbs *linkedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
blobLinkPath, err := lbs.linkPath(lbs.pm, lbs.repository.Name(), dgst)
if err != nil {
return distribution.Descriptor{}, err
}
target, err := lbs.blobStore.readlink(ctx, blobLinkPath)
if err != nil {
switch err := err.(type) {
case driver.PathNotFoundError:
return distribution.Descriptor{}, distribution.ErrBlobUnknown
default:
return distribution.Descriptor{}, err
}
// TODO(stevvooe): For backwards compatibility with data in "_layers", we
// need to hit layerLinkPath, as well. Or, somehow migrate to the new path
// layout.
}
if target != dgst {
// Track when we are doing cross-digest domain lookups. ie, tarsum to sha256.
context.GetLogger(ctx).Warnf("looking up blob with canonical target: %v -> %v", dgst, target)
}
// TODO(stevvooe): Look up repository local mediatype and replace that on
// the returned descriptor.
return lbs.blobStore.statter.Stat(ctx, target)
}
// blobLinkPath provides the path to the blob link, also known as layers.
func blobLinkPath(pm *pathMapper, name string, dgst digest.Digest) (string, error) {
return pm.path(layerLinkPathSpec{name: name, digest: dgst})
}
// manifestRevisionLinkPath provides the path to the manifest revision link.
func manifestRevisionLinkPath(pm *pathMapper, name string, dgst digest.Digest) (string, error) {
return pm.path(layerLinkPathSpec{name: name, digest: dgst})
}

View file

@ -4,88 +4,92 @@ import (
"fmt" "fmt"
"github.com/docker/distribution" "github.com/docker/distribution"
ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/libtrust" "github.com/docker/libtrust"
) )
type manifestStore struct { type manifestStore struct {
repository *repository repository *repository
revisionStore *revisionStore revisionStore *revisionStore
tagStore *tagStore tagStore *tagStore
ctx context.Context
} }
var _ distribution.ManifestService = &manifestStore{} var _ distribution.ManifestService = &manifestStore{}
func (ms *manifestStore) Exists(dgst digest.Digest) (bool, error) { func (ms *manifestStore) Exists(dgst digest.Digest) (bool, error) {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Exists") context.GetLogger(ms.ctx).Debug("(*manifestStore).Exists")
return ms.revisionStore.exists(dgst)
_, err := ms.revisionStore.blobStore.Stat(ms.ctx, dgst)
if err != nil {
if err == distribution.ErrBlobUnknown {
return false, nil
}
return false, err
}
return true, nil
} }
func (ms *manifestStore) Get(dgst digest.Digest) (*manifest.SignedManifest, error) { func (ms *manifestStore) Get(dgst digest.Digest) (*manifest.SignedManifest, error) {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Get") context.GetLogger(ms.ctx).Debug("(*manifestStore).Get")
return ms.revisionStore.get(dgst) return ms.revisionStore.get(ms.ctx, dgst)
} }
func (ms *manifestStore) Put(manifest *manifest.SignedManifest) error { func (ms *manifestStore) Put(manifest *manifest.SignedManifest) error {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Put") context.GetLogger(ms.ctx).Debug("(*manifestStore).Put")
// TODO(stevvooe): Add check here to see if the revision is already
// present in the repository. If it is, we should merge the signatures, do
// a shallow verify (or a full one, doesn't matter) and return an error
// indicating what happened.
// Verify the manifest. // Verify the manifest.
if err := ms.verifyManifest(manifest); err != nil { if err := ms.verifyManifest(ms.ctx, manifest); err != nil {
return err return err
} }
// Store the revision of the manifest // Store the revision of the manifest
revision, err := ms.revisionStore.put(manifest) revision, err := ms.revisionStore.put(ms.ctx, manifest)
if err != nil { if err != nil {
return err return err
} }
// Now, tag the manifest // Now, tag the manifest
return ms.tagStore.tag(manifest.Tag, revision) return ms.tagStore.tag(manifest.Tag, revision.Digest)
} }
// Delete removes the revision of the specified manfiest. // Delete removes the revision of the specified manfiest.
func (ms *manifestStore) Delete(dgst digest.Digest) error { func (ms *manifestStore) Delete(dgst digest.Digest) error {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Delete - unsupported") context.GetLogger(ms.ctx).Debug("(*manifestStore).Delete - unsupported")
return fmt.Errorf("deletion of manifests not supported") return fmt.Errorf("deletion of manifests not supported")
} }
func (ms *manifestStore) Tags() ([]string, error) { func (ms *manifestStore) Tags() ([]string, error) {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Tags") context.GetLogger(ms.ctx).Debug("(*manifestStore).Tags")
return ms.tagStore.tags() return ms.tagStore.tags()
} }
func (ms *manifestStore) ExistsByTag(tag string) (bool, error) { func (ms *manifestStore) ExistsByTag(tag string) (bool, error) {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).ExistsByTag") context.GetLogger(ms.ctx).Debug("(*manifestStore).ExistsByTag")
return ms.tagStore.exists(tag) return ms.tagStore.exists(tag)
} }
func (ms *manifestStore) GetByTag(tag string) (*manifest.SignedManifest, error) { func (ms *manifestStore) GetByTag(tag string) (*manifest.SignedManifest, error) {
ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).GetByTag") context.GetLogger(ms.ctx).Debug("(*manifestStore).GetByTag")
dgst, err := ms.tagStore.resolve(tag) dgst, err := ms.tagStore.resolve(tag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return ms.revisionStore.get(dgst) return ms.revisionStore.get(ms.ctx, dgst)
} }
// verifyManifest ensures that the manifest content is valid from the // verifyManifest ensures that the manifest content is valid from the
// perspective of the registry. It ensures that the signature is valid for the // perspective of the registry. It ensures that the signature is valid for the
// enclosed payload. As a policy, the registry only tries to store valid // enclosed payload. As a policy, the registry only tries to store valid
// content, leaving trust policies of that content up to consumers. // content, leaving trust policies of that content up to consumers.
func (ms *manifestStore) verifyManifest(mnfst *manifest.SignedManifest) error { func (ms *manifestStore) verifyManifest(ctx context.Context, mnfst *manifest.SignedManifest) error {
var errs distribution.ErrManifestVerification var errs distribution.ErrManifestVerification
if mnfst.Name != ms.repository.Name() { if mnfst.Name != ms.repository.Name() {
// TODO(stevvooe): This needs to be an exported error
errs = append(errs, fmt.Errorf("repository name does not match manifest name")) errs = append(errs, fmt.Errorf("repository name does not match manifest name"))
} }
@ -103,18 +107,18 @@ func (ms *manifestStore) verifyManifest(mnfst *manifest.SignedManifest) error {
} }
for _, fsLayer := range mnfst.FSLayers { for _, fsLayer := range mnfst.FSLayers {
exists, err := ms.repository.Layers().Exists(fsLayer.BlobSum) _, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.BlobSum)
if err != nil { if err != nil {
errs = append(errs, err) if err != distribution.ErrBlobUnknown {
} errs = append(errs, err)
}
if !exists { // On error here, we always append unknown blob errors.
errs = append(errs, distribution.ErrUnknownLayer{FSLayer: fsLayer}) errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.BlobSum})
} }
} }
if len(errs) != 0 { if len(errs) != 0 {
// TODO(stevvooe): These need to be recoverable by a caller.
return errs return errs
} }

View file

@ -6,16 +6,15 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/distribution/registry/storage/cache"
"github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/docker/distribution/testutil" "github.com/docker/distribution/testutil"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"golang.org/x/net/context"
) )
type manifestStoreTestEnv struct { type manifestStoreTestEnv struct {
@ -30,7 +29,7 @@ type manifestStoreTestEnv struct {
func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv {
ctx := context.Background() ctx := context.Background()
driver := inmemory.New() driver := inmemory.New()
registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryLayerInfoCache()) registry := NewRegistryWithDriver(ctx, driver, cache.NewInMemoryBlobDescriptorCacheProvider())
repo, err := registry.Repository(ctx, name) repo, err := registry.Repository(ctx, name)
if err != nil { if err != nil {
@ -108,20 +107,33 @@ func TestManifestStorage(t *testing.T) {
t.Fatalf("expected errors putting manifest") t.Fatalf("expected errors putting manifest")
} }
// TODO(stevvooe): We expect errors describing all of the missing layers. switch err := err.(type) {
case distribution.ErrManifestVerification:
if len(err) != 2 {
t.Fatalf("expected 2 verification errors: %#v", err)
}
for _, err := range err {
if _, ok := err.(distribution.ErrManifestBlobUnknown); !ok {
t.Fatalf("unexpected error type: %v", err)
}
}
default:
t.Fatalf("unexpected error verifying manifest: %v", err)
}
// Now, upload the layers that were missing! // Now, upload the layers that were missing!
for dgst, rs := range testLayers { for dgst, rs := range testLayers {
upload, err := env.repository.Layers().Upload() wr, err := env.repository.Blobs(env.ctx).Create(env.ctx)
if err != nil { if err != nil {
t.Fatalf("unexpected error creating test upload: %v", err) t.Fatalf("unexpected error creating test upload: %v", err)
} }
if _, err := io.Copy(upload, rs); err != nil { if _, err := io.Copy(wr, rs); err != nil {
t.Fatalf("unexpected error copying to upload: %v", err) t.Fatalf("unexpected error copying to upload: %v", err)
} }
if _, err := upload.Finish(dgst); err != nil { if _, err := wr.Commit(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil {
t.Fatalf("unexpected error finishing upload: %v", err) t.Fatalf("unexpected error finishing upload: %v", err)
} }
} }

View file

@ -30,7 +30,7 @@ const storagePathVersion = "v2"
// -> <algorithm>/<hex digest>/link // -> <algorithm>/<hex digest>/link
// -> _layers/ // -> _layers/
// <layer links to blob store> // <layer links to blob store>
// -> _uploads/<uuid> // -> _uploads/<id>
// data // data
// startedat // startedat
// hashstates/<algorithm>/<offset> // hashstates/<algorithm>/<offset>
@ -47,7 +47,7 @@ const storagePathVersion = "v2"
// is just a directory of layers which are "linked" into a repository. A layer // is just a directory of layers which are "linked" into a repository. A layer
// can only be accessed through a qualified repository name if it is linked in // can only be accessed through a qualified repository name if it is linked in
// the repository. Uploads of layers are managed in the uploads directory, // the repository. Uploads of layers are managed in the uploads directory,
// which is key by upload uuid. When all data for an upload is received, the // which is key by upload id. When all data for an upload is received, the
// data is moved into the blob store and the upload directory is deleted. // data is moved into the blob store and the upload directory is deleted.
// Abandoned uploads can be garbage collected by reading the startedat file // Abandoned uploads can be garbage collected by reading the startedat file
// and removing uploads that have been active for longer than a certain time. // and removing uploads that have been active for longer than a certain time.
@ -80,20 +80,21 @@ const storagePathVersion = "v2"
// manifestTagIndexEntryPathSpec: <root>/v2/repositories/<name>/_manifests/tags/<tag>/index/<algorithm>/<hex digest>/ // manifestTagIndexEntryPathSpec: <root>/v2/repositories/<name>/_manifests/tags/<tag>/index/<algorithm>/<hex digest>/
// manifestTagIndexEntryLinkPathSpec: <root>/v2/repositories/<name>/_manifests/tags/<tag>/index/<algorithm>/<hex digest>/link // manifestTagIndexEntryLinkPathSpec: <root>/v2/repositories/<name>/_manifests/tags/<tag>/index/<algorithm>/<hex digest>/link
// //
// Layers: // Blobs:
// //
// layerLinkPathSpec: <root>/v2/repositories/<name>/_layers/tarsum/<tarsum version>/<tarsum hash alg>/<tarsum hash>/link // layerLinkPathSpec: <root>/v2/repositories/<name>/_layers/<algorithm>/<hex digest>/link
// //
// Uploads: // Uploads:
// //
// uploadDataPathSpec: <root>/v2/repositories/<name>/_uploads/<uuid>/data // uploadDataPathSpec: <root>/v2/repositories/<name>/_uploads/<id>/data
// uploadStartedAtPathSpec: <root>/v2/repositories/<name>/_uploads/<uuid>/startedat // uploadStartedAtPathSpec: <root>/v2/repositories/<name>/_uploads/<id>/startedat
// uploadHashStatePathSpec: <root>/v2/repositories/<name>/_uploads/<uuid>/hashstates/<algorithm>/<offset> // uploadHashStatePathSpec: <root>/v2/repositories/<name>/_uploads/<id>/hashstates/<algorithm>/<offset>
// //
// Blob Store: // Blob Store:
// //
// blobPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest> // blobPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>
// blobDataPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data // blobDataPathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
// blobMediaTypePathSpec: <root>/v2/blobs/<algorithm>/<first two hex bytes of digest>/<hex digest>/data
// //
// For more information on the semantic meaning of each path and their // For more information on the semantic meaning of each path and their
// contents, please see the path spec documentation. // contents, please see the path spec documentation.
@ -234,9 +235,14 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
return "", err return "", err
} }
layerLinkPathComponents := append(repoPrefix, v.name, "_layers") // TODO(stevvooe): Right now, all blobs are linked under "_layers". If
// we have future migrations, we may want to rename this to "_blobs".
// A migration strategy would simply leave existing items in place and
// write the new paths, commit a file then delete the old files.
return path.Join(path.Join(append(layerLinkPathComponents, components...)...), "link"), nil blobLinkPathComponents := append(repoPrefix, v.name, "_layers")
return path.Join(path.Join(append(blobLinkPathComponents, components...)...), "link"), nil
case blobDataPathSpec: case blobDataPathSpec:
components, err := digestPathComponents(v.digest, true) components, err := digestPathComponents(v.digest, true)
if err != nil { if err != nil {
@ -248,15 +254,15 @@ func (pm *pathMapper) path(spec pathSpec) (string, error) {
return path.Join(append(blobPathPrefix, components...)...), nil return path.Join(append(blobPathPrefix, components...)...), nil
case uploadDataPathSpec: case uploadDataPathSpec:
return path.Join(append(repoPrefix, v.name, "_uploads", v.uuid, "data")...), nil return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "data")...), nil
case uploadStartedAtPathSpec: case uploadStartedAtPathSpec:
return path.Join(append(repoPrefix, v.name, "_uploads", v.uuid, "startedat")...), nil return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "startedat")...), nil
case uploadHashStatePathSpec: case uploadHashStatePathSpec:
offset := fmt.Sprintf("%d", v.offset) offset := fmt.Sprintf("%d", v.offset)
if v.list { if v.list {
offset = "" // Limit to the prefix for listing offsets. offset = "" // Limit to the prefix for listing offsets.
} }
return path.Join(append(repoPrefix, v.name, "_uploads", v.uuid, "hashstates", v.alg, offset)...), nil return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "hashstates", v.alg, offset)...), nil
case repositoriesRootPathSpec: case repositoriesRootPathSpec:
return path.Join(repoPrefix...), nil return path.Join(repoPrefix...), nil
default: default:
@ -367,8 +373,8 @@ type manifestTagIndexEntryLinkPathSpec struct {
func (manifestTagIndexEntryLinkPathSpec) pathSpec() {} func (manifestTagIndexEntryLinkPathSpec) pathSpec() {}
// layerLink specifies a path for a layer link, which is a file with a blob // blobLinkPathSpec specifies a path for a blob link, which is a file with a
// id. The layer link will contain a content addressable blob id reference // blob id. The blob link will contain a content addressable blob id reference
// into the blob store. The format of the contents is as follows: // into the blob store. The format of the contents is as follows:
// //
// <algorithm>:<hex digest of layer data> // <algorithm>:<hex digest of layer data>
@ -377,7 +383,7 @@ func (manifestTagIndexEntryLinkPathSpec) pathSpec() {}
// //
// sha256:96443a84ce518ac22acb2e985eda402b58ac19ce6f91980bde63726a79d80b36 // sha256:96443a84ce518ac22acb2e985eda402b58ac19ce6f91980bde63726a79d80b36
// //
// This says indicates that there is a blob with the id/digest, calculated via // This indicates that there is a blob with the id/digest, calculated via
// sha256 that can be fetched from the blob store. // sha256 that can be fetched from the blob store.
type layerLinkPathSpec struct { type layerLinkPathSpec struct {
name string name string
@ -415,7 +421,7 @@ func (blobDataPathSpec) pathSpec() {}
// uploads. // uploads.
type uploadDataPathSpec struct { type uploadDataPathSpec struct {
name string name string
uuid string id string
} }
func (uploadDataPathSpec) pathSpec() {} func (uploadDataPathSpec) pathSpec() {}
@ -429,7 +435,7 @@ func (uploadDataPathSpec) pathSpec() {}
// the client to enforce time out policies. // the client to enforce time out policies.
type uploadStartedAtPathSpec struct { type uploadStartedAtPathSpec struct {
name string name string
uuid string id string
} }
func (uploadStartedAtPathSpec) pathSpec() {} func (uploadStartedAtPathSpec) pathSpec() {}
@ -437,10 +443,10 @@ func (uploadStartedAtPathSpec) pathSpec() {}
// uploadHashStatePathSpec defines the path parameters for the file that stores // uploadHashStatePathSpec defines the path parameters for the file that stores
// the hash function state of an upload at a specific byte offset. If `list` is // the hash function state of an upload at a specific byte offset. If `list` is
// set, then the path mapper will generate a list prefix for all hash state // set, then the path mapper will generate a list prefix for all hash state
// offsets for the upload identified by the name, uuid, and alg. // offsets for the upload identified by the name, id, and alg.
type uploadHashStatePathSpec struct { type uploadHashStatePathSpec struct {
name string name string
uuid string id string
alg string alg string
offset int64 offset int64
list bool list bool

View file

@ -111,14 +111,14 @@ func TestPathMapper(t *testing.T) {
{ {
spec: uploadDataPathSpec{ spec: uploadDataPathSpec{
name: "foo/bar", name: "foo/bar",
uuid: "asdf-asdf-asdf-adsf", id: "asdf-asdf-asdf-adsf",
}, },
expected: "/pathmapper-test/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/data", expected: "/pathmapper-test/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/data",
}, },
{ {
spec: uploadStartedAtPathSpec{ spec: uploadStartedAtPathSpec{
name: "foo/bar", name: "foo/bar",
uuid: "asdf-asdf-asdf-adsf", id: "asdf-asdf-asdf-adsf",
}, },
expected: "/pathmapper-test/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/startedat", expected: "/pathmapper-test/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/startedat",
}, },

View file

@ -24,7 +24,7 @@ func testUploadFS(t *testing.T, numUploads int, repoName string, startedAt time.
} }
func addUploads(ctx context.Context, t *testing.T, d driver.StorageDriver, uploadID, repo string, startedAt time.Time) { func addUploads(ctx context.Context, t *testing.T, d driver.StorageDriver, uploadID, repo string, startedAt time.Time) {
dataPath, err := pm.path(uploadDataPathSpec{name: repo, uuid: uploadID}) dataPath, err := pm.path(uploadDataPathSpec{name: repo, id: uploadID})
if err != nil { if err != nil {
t.Fatalf("Unable to resolve path") t.Fatalf("Unable to resolve path")
} }
@ -32,7 +32,7 @@ func addUploads(ctx context.Context, t *testing.T, d driver.StorageDriver, uploa
t.Fatalf("Unable to write data file") t.Fatalf("Unable to write data file")
} }
startedAtPath, err := pm.path(uploadStartedAtPathSpec{name: repo, uuid: uploadID}) startedAtPath, err := pm.path(uploadStartedAtPathSpec{name: repo, id: uploadID})
if err != nil { if err != nil {
t.Fatalf("Unable to resolve path") t.Fatalf("Unable to resolve path")
} }
@ -115,7 +115,7 @@ func TestPurgeOnlyUploads(t *testing.T) {
// Create a directory tree outside _uploads and ensure // Create a directory tree outside _uploads and ensure
// these files aren't deleted. // these files aren't deleted.
dataPath, err := pm.path(uploadDataPathSpec{name: "test-repo", uuid: uuid.New()}) dataPath, err := pm.path(uploadDataPathSpec{name: "test-repo", id: uuid.New()})
if err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }

View file

@ -2,38 +2,53 @@ package storage
import ( import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache"
storagedriver "github.com/docker/distribution/registry/storage/driver" storagedriver "github.com/docker/distribution/registry/storage/driver"
"golang.org/x/net/context"
) )
// registry is the top-level implementation of Registry for use in the storage // registry is the top-level implementation of Registry for use in the storage
// package. All instances should descend from this object. // package. All instances should descend from this object.
type registry struct { type registry struct {
driver storagedriver.StorageDriver blobStore *blobStore
pm *pathMapper blobServer distribution.BlobServer
blobStore *blobStore statter distribution.BlobStatter // global statter service.
layerInfoCache cache.LayerInfoCache blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider
} }
// NewRegistryWithDriver creates a new registry instance from the provided // NewRegistryWithDriver creates a new registry instance from the provided
// driver. The resulting registry may be shared by multiple goroutines but is // driver. The resulting registry may be shared by multiple goroutines but is
// cheap to allocate. // cheap to allocate.
func NewRegistryWithDriver(ctx context.Context, driver storagedriver.StorageDriver, layerInfoCache cache.LayerInfoCache) distribution.Namespace { func NewRegistryWithDriver(ctx context.Context, driver storagedriver.StorageDriver, blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider) distribution.Namespace {
bs := &blobStore{
// create global statter, with cache.
var statter distribution.BlobStatter = &blobStatter{
driver: driver, driver: driver,
pm: defaultPathMapper, pm: defaultPathMapper,
ctx: ctx, }
if blobDescriptorCacheProvider != nil {
statter = &cachedBlobStatter{
cache: blobDescriptorCacheProvider,
backend: statter,
}
}
bs := &blobStore{
driver: driver,
pm: defaultPathMapper,
statter: statter,
} }
return &registry{ return &registry{
driver: driver,
blobStore: bs, blobStore: bs,
blobServer: &blobServer{
// TODO(sday): This should be configurable. driver: driver,
pm: defaultPathMapper, statter: statter,
layerInfoCache: layerInfoCache, pathFn: bs.path,
},
blobDescriptorCacheProvider: blobDescriptorCacheProvider,
} }
} }
@ -54,18 +69,29 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
} }
} }
var descriptorCache distribution.BlobDescriptorService
if reg.blobDescriptorCacheProvider != nil {
var err error
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(name)
if err != nil {
return nil, err
}
}
return &repository{ return &repository{
ctx: ctx, ctx: ctx,
registry: reg, registry: reg,
name: name, name: name,
descriptorCache: descriptorCache,
}, nil }, nil
} }
// repository provides name-scoped access to various services. // repository provides name-scoped access to various services.
type repository struct { type repository struct {
*registry *registry
ctx context.Context ctx context.Context
name string name string
descriptorCache distribution.BlobDescriptorService
} }
// Name returns the name of the repository. // Name returns the name of the repository.
@ -78,47 +104,68 @@ func (repo *repository) Name() string {
// to a request local. // to a request local.
func (repo *repository) Manifests() distribution.ManifestService { func (repo *repository) Manifests() distribution.ManifestService {
return &manifestStore{ return &manifestStore{
ctx: repo.ctx,
repository: repo, repository: repo,
revisionStore: &revisionStore{ revisionStore: &revisionStore{
ctx: repo.ctx,
repository: repo, repository: repo,
blobStore: &linkedBlobStore{
ctx: repo.ctx,
blobStore: repo.blobStore,
repository: repo,
statter: &linkedBlobStatter{
blobStore: repo.blobStore,
repository: repo,
linkPath: manifestRevisionLinkPath,
},
// TODO(stevvooe): linkPath limits this blob store to only
// manifests. This instance cannot be used for blob checks.
linkPath: manifestRevisionLinkPath,
},
}, },
tagStore: &tagStore{ tagStore: &tagStore{
ctx: repo.ctx,
repository: repo, repository: repo,
blobStore: repo.registry.blobStore,
}, },
} }
} }
// Layers returns an instance of the LayerService. Instantiation is cheap and // Blobs returns an instance of the BlobStore. Instantiation is cheap and
// may be context sensitive in the future. The instance should be used similar // may be context sensitive in the future. The instance should be used similar
// to a request local. // to a request local.
func (repo *repository) Layers() distribution.LayerService { func (repo *repository) Blobs(ctx context.Context) distribution.BlobStore {
ls := &layerStore{ var statter distribution.BlobStatter = &linkedBlobStatter{
blobStore: repo.blobStore,
repository: repo, repository: repo,
linkPath: blobLinkPath,
} }
if repo.registry.layerInfoCache != nil { if repo.descriptorCache != nil {
// TODO(stevvooe): This is not the best place to setup a cache. We would statter = &cachedBlobStatter{
// really like to decouple the cache from the backend but also have the cache: repo.descriptorCache,
// manifeset service use the layer service cache. For now, we can simply backend: statter,
// integrate the cache directly. The main issue is that we have layer
// access and layer data coupled in a single object. Work is already under
// way to decouple this.
return &cachedLayerService{
LayerService: ls,
repository: repo,
ctx: repo.ctx,
driver: repo.driver,
blobStore: repo.blobStore,
cache: repo.registry.layerInfoCache,
} }
} }
return ls return &linkedBlobStore{
blobStore: repo.blobStore,
blobServer: repo.blobServer,
statter: statter,
repository: repo,
ctx: ctx,
// TODO(stevvooe): linkPath limits this blob store to only layers.
// This instance cannot be used for manifest checks.
linkPath: blobLinkPath,
}
} }
func (repo *repository) Signatures() distribution.SignatureService { func (repo *repository) Signatures() distribution.SignatureService {
return &signatureStore{ return &signatureStore{
repository: repo, repository: repo,
blobStore: repo.blobStore,
ctx: repo.ctx,
} }
} }

View file

@ -3,8 +3,8 @@ package storage
import ( import (
"encoding/json" "encoding/json"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest"
"github.com/docker/libtrust" "github.com/docker/libtrust"
@ -12,47 +12,56 @@ import (
// revisionStore supports storing and managing manifest revisions. // revisionStore supports storing and managing manifest revisions.
type revisionStore struct { type revisionStore struct {
*repository repository *repository
blobStore *linkedBlobStore
ctx context.Context
} }
// exists returns true if the revision is available in the named repository. func newRevisionStore(ctx context.Context, repo *repository, blobStore *blobStore) *revisionStore {
func (rs *revisionStore) exists(revision digest.Digest) (bool, error) { return &revisionStore{
revpath, err := rs.pm.path(manifestRevisionPathSpec{ ctx: ctx,
name: rs.Name(), repository: repo,
revision: revision, blobStore: &linkedBlobStore{
}) blobStore: blobStore,
repository: repo,
if err != nil { ctx: ctx,
return false, err linkPath: manifestRevisionLinkPath,
},
} }
exists, err := exists(rs.repository.ctx, rs.driver, revpath)
if err != nil {
return false, err
}
return exists, nil
} }
// get retrieves the manifest, keyed by revision digest. // get retrieves the manifest, keyed by revision digest.
func (rs *revisionStore) get(revision digest.Digest) (*manifest.SignedManifest, error) { func (rs *revisionStore) get(ctx context.Context, revision digest.Digest) (*manifest.SignedManifest, error) {
// Ensure that this revision is available in this repository. // Ensure that this revision is available in this repository.
if exists, err := rs.exists(revision); err != nil { _, err := rs.blobStore.Stat(ctx, revision)
return nil, err if err != nil {
} else if !exists { if err == distribution.ErrBlobUnknown {
return nil, distribution.ErrUnknownManifestRevision{ return nil, distribution.ErrManifestUnknownRevision{
Name: rs.Name(), Name: rs.repository.Name(),
Revision: revision, Revision: revision,
}
} }
return nil, err
} }
content, err := rs.blobStore.get(revision) // TODO(stevvooe): Need to check descriptor from above to ensure that the
// mediatype is as we expect for the manifest store.
content, err := rs.blobStore.Get(ctx, revision)
if err != nil { if err != nil {
if err == distribution.ErrBlobUnknown {
return nil, distribution.ErrManifestUnknownRevision{
Name: rs.repository.Name(),
Revision: revision,
}
}
return nil, err return nil, err
} }
// Fetch the signatures for the manifest // Fetch the signatures for the manifest
signatures, err := rs.Signatures().Get(revision) signatures, err := rs.repository.Signatures().Get(revision)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,69 +87,34 @@ func (rs *revisionStore) get(revision digest.Digest) (*manifest.SignedManifest,
// put stores the manifest in the repository, if not already present. Any // put stores the manifest in the repository, if not already present. Any
// updated signatures will be stored, as well. // updated signatures will be stored, as well.
func (rs *revisionStore) put(sm *manifest.SignedManifest) (digest.Digest, error) { func (rs *revisionStore) put(ctx context.Context, sm *manifest.SignedManifest) (distribution.Descriptor, error) {
// Resolve the payload in the manifest. // Resolve the payload in the manifest.
payload, err := sm.Payload() payload, err := sm.Payload()
if err != nil { if err != nil {
return "", err return distribution.Descriptor{}, err
} }
// Digest and store the manifest payload in the blob store. // Digest and store the manifest payload in the blob store.
revision, err := rs.blobStore.put(payload) revision, err := rs.blobStore.Put(ctx, manifest.ManifestMediaType, payload)
if err != nil { if err != nil {
logrus.Errorf("error putting payload into blobstore: %v", err) context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err)
return "", err return distribution.Descriptor{}, err
} }
// Link the revision into the repository. // Link the revision into the repository.
if err := rs.link(revision); err != nil { if err := rs.blobStore.linkBlob(ctx, revision); err != nil {
return "", err return distribution.Descriptor{}, err
} }
// Grab each json signature and store them. // Grab each json signature and store them.
signatures, err := sm.Signatures() signatures, err := sm.Signatures()
if err != nil { if err != nil {
return "", err return distribution.Descriptor{}, err
} }
if err := rs.Signatures().Put(revision, signatures...); err != nil { if err := rs.repository.Signatures().Put(revision.Digest, signatures...); err != nil {
return "", err return distribution.Descriptor{}, err
} }
return revision, nil return revision, nil
} }
// link links the revision into the repository.
func (rs *revisionStore) link(revision digest.Digest) error {
revisionPath, err := rs.pm.path(manifestRevisionLinkPathSpec{
name: rs.Name(),
revision: revision,
})
if err != nil {
return err
}
if exists, err := exists(rs.repository.ctx, rs.driver, revisionPath); err != nil {
return err
} else if exists {
// Revision has already been linked!
return nil
}
return rs.blobStore.link(revisionPath, revision)
}
// delete removes the specified manifest revision from storage.
func (rs *revisionStore) delete(revision digest.Digest) error {
revisionPath, err := rs.pm.path(manifestRevisionPathSpec{
name: rs.Name(),
revision: revision,
})
if err != nil {
return err
}
return rs.driver.Delete(rs.repository.ctx, revisionPath)
}

View file

@ -10,14 +10,24 @@ import (
) )
type signatureStore struct { type signatureStore struct {
*repository repository *repository
blobStore *blobStore
ctx context.Context
}
func newSignatureStore(ctx context.Context, repo *repository, blobStore *blobStore) *signatureStore {
return &signatureStore{
ctx: ctx,
repository: repo,
blobStore: blobStore,
}
} }
var _ distribution.SignatureService = &signatureStore{} var _ distribution.SignatureService = &signatureStore{}
func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) { func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) {
signaturesPath, err := s.pm.path(manifestSignaturesPathSpec{ signaturesPath, err := s.blobStore.pm.path(manifestSignaturesPathSpec{
name: s.Name(), name: s.repository.Name(),
revision: dgst, revision: dgst,
}) })
@ -30,7 +40,7 @@ func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) {
// can be eliminated by implementing listAll on drivers. // can be eliminated by implementing listAll on drivers.
signaturesPath = path.Join(signaturesPath, "sha256") signaturesPath = path.Join(signaturesPath, "sha256")
signaturePaths, err := s.driver.List(s.repository.ctx, signaturesPath) signaturePaths, err := s.blobStore.driver.List(s.ctx, signaturesPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -43,27 +53,32 @@ func (s *signatureStore) Get(dgst digest.Digest) ([][]byte, error) {
} }
ch := make(chan result) ch := make(chan result)
bs := s.linkedBlobStore(s.ctx, dgst)
for i, sigPath := range signaturePaths { for i, sigPath := range signaturePaths {
// Append the link portion sigdgst, err := digest.ParseDigest("sha256:" + path.Base(sigPath))
sigPath = path.Join(sigPath, "link") if err != nil {
context.GetLogger(s.ctx).Errorf("could not get digest from path: %q, skipping", sigPath)
continue
}
wg.Add(1) wg.Add(1)
go func(idx int, sigPath string) { go func(idx int, sigdgst digest.Digest) {
defer wg.Done() defer wg.Done()
context.GetLogger(s.ctx). context.GetLogger(s.ctx).
Debugf("fetching signature from %q", sigPath) Debugf("fetching signature %q", sigdgst)
r := result{index: idx} r := result{index: idx}
if p, err := s.blobStore.linked(sigPath); err != nil {
if p, err := bs.Get(s.ctx, sigdgst); err != nil {
context.GetLogger(s.ctx). context.GetLogger(s.ctx).
Errorf("error fetching signature from %q: %v", sigPath, err) Errorf("error fetching signature %q: %v", sigdgst, err)
r.err = err r.err = err
} else { } else {
r.signature = p r.signature = p
} }
ch <- r ch <- r
}(i, sigPath) }(i, sigdgst)
} }
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
@ -91,25 +106,36 @@ loop:
} }
func (s *signatureStore) Put(dgst digest.Digest, signatures ...[]byte) error { func (s *signatureStore) Put(dgst digest.Digest, signatures ...[]byte) error {
bs := s.linkedBlobStore(s.ctx, dgst)
for _, signature := range signatures { for _, signature := range signatures {
signatureDigest, err := s.blobStore.put(signature) if _, err := bs.Put(s.ctx, "application/json", signature); err != nil {
if err != nil {
return err
}
signaturePath, err := s.pm.path(manifestSignatureLinkPathSpec{
name: s.Name(),
revision: dgst,
signature: signatureDigest,
})
if err != nil {
return err
}
if err := s.blobStore.link(signaturePath, signatureDigest); err != nil {
return err return err
} }
} }
return nil return nil
} }
// namedBlobStore returns the namedBlobStore of the signatures for the
// manifest with the given digest. Effectively, each singature link path
// layout is a unique linked blob store.
func (s *signatureStore) linkedBlobStore(ctx context.Context, revision digest.Digest) *linkedBlobStore {
linkpath := func(pm *pathMapper, name string, dgst digest.Digest) (string, error) {
return pm.path(manifestSignatureLinkPathSpec{
name: name,
revision: revision,
signature: dgst,
})
}
return &linkedBlobStore{
ctx: ctx,
repository: s.repository,
blobStore: s.blobStore,
statter: &linkedBlobStatter{
blobStore: s.blobStore,
repository: s.repository,
linkPath: linkpath,
},
linkPath: linkpath,
}
}

View file

@ -4,31 +4,33 @@ import (
"path" "path"
"github.com/docker/distribution" "github.com/docker/distribution"
// "github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
storagedriver "github.com/docker/distribution/registry/storage/driver" storagedriver "github.com/docker/distribution/registry/storage/driver"
) )
// tagStore provides methods to manage manifest tags in a backend storage driver. // tagStore provides methods to manage manifest tags in a backend storage driver.
type tagStore struct { type tagStore struct {
*repository repository *repository
blobStore *blobStore
ctx context.Context
} }
// tags lists the manifest tags for the specified repository. // tags lists the manifest tags for the specified repository.
func (ts *tagStore) tags() ([]string, error) { func (ts *tagStore) tags() ([]string, error) {
p, err := ts.pm.path(manifestTagPathSpec{ p, err := ts.blobStore.pm.path(manifestTagPathSpec{
name: ts.name, name: ts.repository.Name(),
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
var tags []string var tags []string
entries, err := ts.driver.List(ts.repository.ctx, p) entries, err := ts.blobStore.driver.List(ts.ctx, p)
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case storagedriver.PathNotFoundError: case storagedriver.PathNotFoundError:
return nil, distribution.ErrRepositoryUnknown{Name: ts.name} return nil, distribution.ErrRepositoryUnknown{Name: ts.repository.Name()}
default: default:
return nil, err return nil, err
} }
@ -45,15 +47,15 @@ func (ts *tagStore) tags() ([]string, error) {
// exists returns true if the specified manifest tag exists in the repository. // exists returns true if the specified manifest tag exists in the repository.
func (ts *tagStore) exists(tag string) (bool, error) { func (ts *tagStore) exists(tag string) (bool, error) {
tagPath, err := ts.pm.path(manifestTagCurrentPathSpec{ tagPath, err := ts.blobStore.pm.path(manifestTagCurrentPathSpec{
name: ts.Name(), name: ts.repository.Name(),
tag: tag, tag: tag,
}) })
if err != nil { if err != nil {
return false, err return false, err
} }
exists, err := exists(ts.repository.ctx, ts.driver, tagPath) exists, err := exists(ts.ctx, ts.blobStore.driver, tagPath)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -64,18 +66,8 @@ func (ts *tagStore) exists(tag string) (bool, error) {
// tag tags the digest with the given tag, updating the the store to point at // tag tags the digest with the given tag, updating the the store to point at
// the current tag. The digest must point to a manifest. // the current tag. The digest must point to a manifest.
func (ts *tagStore) tag(tag string, revision digest.Digest) error { func (ts *tagStore) tag(tag string, revision digest.Digest) error {
indexEntryPath, err := ts.pm.path(manifestTagIndexEntryLinkPathSpec{ currentPath, err := ts.blobStore.pm.path(manifestTagCurrentPathSpec{
name: ts.Name(), name: ts.repository.Name(),
tag: tag,
revision: revision,
})
if err != nil {
return err
}
currentPath, err := ts.pm.path(manifestTagCurrentPathSpec{
name: ts.Name(),
tag: tag, tag: tag,
}) })
@ -83,77 +75,69 @@ func (ts *tagStore) tag(tag string, revision digest.Digest) error {
return err return err
} }
nbs := ts.linkedBlobStore(ts.ctx, tag)
// Link into the index // Link into the index
if err := ts.blobStore.link(indexEntryPath, revision); err != nil { if err := nbs.linkBlob(ts.ctx, distribution.Descriptor{Digest: revision}); err != nil {
return err return err
} }
// Overwrite the current link // Overwrite the current link
return ts.blobStore.link(currentPath, revision) return ts.blobStore.link(ts.ctx, currentPath, revision)
} }
// resolve the current revision for name and tag. // resolve the current revision for name and tag.
func (ts *tagStore) resolve(tag string) (digest.Digest, error) { func (ts *tagStore) resolve(tag string) (digest.Digest, error) {
currentPath, err := ts.pm.path(manifestTagCurrentPathSpec{ currentPath, err := ts.blobStore.pm.path(manifestTagCurrentPathSpec{
name: ts.Name(), name: ts.repository.Name(),
tag: tag, tag: tag,
}) })
if err != nil { if err != nil {
return "", err return "", err
} }
if exists, err := exists(ts.repository.ctx, ts.driver, currentPath); err != nil { revision, err := ts.blobStore.readlink(ts.ctx, currentPath)
return "", err
} else if !exists {
return "", distribution.ErrManifestUnknown{Name: ts.Name(), Tag: tag}
}
revision, err := ts.blobStore.readlink(currentPath)
if err != nil { if err != nil {
switch err.(type) {
case storagedriver.PathNotFoundError:
return "", distribution.ErrManifestUnknown{Name: ts.repository.Name(), Tag: tag}
}
return "", err return "", err
} }
return revision, nil return revision, nil
} }
// revisions returns all revisions with the specified name and tag.
func (ts *tagStore) revisions(tag string) ([]digest.Digest, error) {
manifestTagIndexPath, err := ts.pm.path(manifestTagIndexPathSpec{
name: ts.Name(),
tag: tag,
})
if err != nil {
return nil, err
}
// TODO(stevvooe): Need to append digest alg to get listing of revisions.
manifestTagIndexPath = path.Join(manifestTagIndexPath, "sha256")
entries, err := ts.driver.List(ts.repository.ctx, manifestTagIndexPath)
if err != nil {
return nil, err
}
var revisions []digest.Digest
for _, entry := range entries {
revisions = append(revisions, digest.NewDigestFromHex("sha256", path.Base(entry)))
}
return revisions, nil
}
// delete removes the tag from repository, including the history of all // delete removes the tag from repository, including the history of all
// revisions that have the specified tag. // revisions that have the specified tag.
func (ts *tagStore) delete(tag string) error { func (ts *tagStore) delete(tag string) error {
tagPath, err := ts.pm.path(manifestTagPathSpec{ tagPath, err := ts.blobStore.pm.path(manifestTagPathSpec{
name: ts.Name(), name: ts.repository.Name(),
tag: tag, tag: tag,
}) })
if err != nil { if err != nil {
return err return err
} }
return ts.driver.Delete(ts.repository.ctx, tagPath) return ts.blobStore.driver.Delete(ts.ctx, tagPath)
}
// namedBlobStore returns the namedBlobStore for the named tag, allowing one
// to index manifest blobs by tag name. While the tag store doesn't map
// precisely to the linked blob store, using this ensures the links are
// managed via the same code path.
func (ts *tagStore) linkedBlobStore(ctx context.Context, tag string) *linkedBlobStore {
return &linkedBlobStore{
blobStore: ts.blobStore,
repository: ts.repository,
ctx: ctx,
linkPath: func(pm *pathMapper, name string, dgst digest.Digest) (string, error) {
return pm.path(manifestTagIndexEntryLinkPathSpec{
name: name,
tag: tag,
revision: dgst,
})
},
}
} }

21
docs/storage/util.go Normal file
View file

@ -0,0 +1,21 @@
package storage
import (
"github.com/docker/distribution/context"
"github.com/docker/distribution/registry/storage/driver"
)
// Exists provides a utility method to test whether or not a path exists in
// the given driver.
func exists(ctx context.Context, drv driver.StorageDriver, path string) (bool, error) {
if _, err := drv.Stat(ctx, path); err != nil {
switch err := err.(type) {
case driver.PathNotFoundError:
return false, nil
default:
return false, err
}
}
return true, nil
}