Merge pull request #769 from stevvooe/layer-http-api
Initial implementation of Layer API
This commit is contained in:
commit
b60ee6dfa0
16 changed files with 914 additions and 241 deletions
236
api_test.go
Normal file
236
api_test.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
_ "github.com/docker/docker-registry/storagedriver/inmemory"
|
||||||
|
|
||||||
|
"github.com/gorilla/handlers"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/common/testutil"
|
||||||
|
"github.com/docker/docker-registry/configuration"
|
||||||
|
"github.com/docker/docker-registry/digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLayerAPI conducts a full of the of the layer api.
|
||||||
|
func TestLayerAPI(t *testing.T) {
|
||||||
|
// TODO(stevvooe): This test code is complete junk but it should cover the
|
||||||
|
// complete flow. This must be broken down and checked against the
|
||||||
|
// specification *before* we submit the final to docker core.
|
||||||
|
|
||||||
|
config := configuration.Configuration{
|
||||||
|
Storage: configuration.Storage{
|
||||||
|
"inmemory": configuration.Parameters{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app := NewApp(config)
|
||||||
|
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
|
||||||
|
router := v2APIRouter()
|
||||||
|
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error parsing server url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageName := "foo/bar"
|
||||||
|
// "build" our layer file
|
||||||
|
layerFile, tarSumStr, err := testutil.CreateRandomTarFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating random layer file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
layerDigest := digest.Digest(tarSumStr)
|
||||||
|
|
||||||
|
// -----------------------------------
|
||||||
|
// Test fetch for non-existent content
|
||||||
|
r, err := router.GetRoute(routeNameBlob).Host(u.Host).
|
||||||
|
URL("name", imageName,
|
||||||
|
"digest", tarSumStr)
|
||||||
|
|
||||||
|
resp, err := http.Get(r.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error fetching non-existent layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
break // expected
|
||||||
|
default:
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status fetching non-existent layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Test head request for non-existent content
|
||||||
|
resp, err = http.Head(r.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
break // expected
|
||||||
|
default:
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status checking head on non-existent layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Upload a layer
|
||||||
|
r, err = router.GetRoute(routeNameBlobUpload).Host(u.Host).
|
||||||
|
URL("name", imageName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting layer upload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = http.Post(r.String(), "", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error starting layer upload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status starting layer upload: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Header.Get("Location") == "" { // TODO(stevvooe): Need better check here.
|
||||||
|
t.Fatalf("unexpected Location: %q != %q", resp.Header.Get("Location"), "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Header.Get("Content-Length") != "0" {
|
||||||
|
t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
layerLength, _ := layerFile.Seek(0, os.SEEK_END)
|
||||||
|
layerFile.Seek(0, os.SEEK_SET)
|
||||||
|
|
||||||
|
uploadURLStr := resp.Header.Get("Location")
|
||||||
|
|
||||||
|
// TODO(sday): Cancel the layer upload here and restart.
|
||||||
|
|
||||||
|
query := url.Values{
|
||||||
|
"digest": []string{layerDigest.String()},
|
||||||
|
"length": []string{fmt.Sprint(layerLength)},
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadURL, err := url.Parse(uploadURLStr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error parsing url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
// Just do a monolithic upload
|
||||||
|
req, err := http.NewRequest("PUT", uploadURL.String(), layerFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating new request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error doing put: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusCreated:
|
||||||
|
break // expected
|
||||||
|
default:
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status putting chunk: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Header.Get("Location") == "" {
|
||||||
|
t.Fatalf("unexpected Location: %q", resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Header.Get("Content-Length") != "0" {
|
||||||
|
t.Fatalf("unexpected content-length: %q != %q", resp.Header.Get("Content-Length"), "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
layerURL := resp.Header.Get("Location")
|
||||||
|
|
||||||
|
// ------------------------
|
||||||
|
// Use a head request to see if the layer exists.
|
||||||
|
resp, err = http.Head(layerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error checking head on non-existent layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
break // expected
|
||||||
|
default:
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status checking head on layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("fetch the layer")
|
||||||
|
// ----------------
|
||||||
|
// Fetch the layer!
|
||||||
|
resp, err = http.Get(layerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error fetching layer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
break // expected
|
||||||
|
default:
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("unexpected status fetching layer: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the body
|
||||||
|
verifier := digest.NewDigestVerifier(layerDigest)
|
||||||
|
io.Copy(verifier, resp.Body)
|
||||||
|
|
||||||
|
if !verifier.Verified() {
|
||||||
|
d, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected status checking head on layer ayo!: %v, %v", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("response:\n%s", string(d))
|
||||||
|
t.Fatalf("response body did not pass verification")
|
||||||
|
}
|
||||||
|
}
|
47
app.go
47
app.go
|
@ -3,7 +3,11 @@ package registry
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/storagedriver"
|
||||||
|
"github.com/docker/docker-registry/storagedriver/factory"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/configuration"
|
"github.com/docker/docker-registry/configuration"
|
||||||
|
"github.com/docker/docker-registry/storage"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
@ -16,6 +20,12 @@ type App struct {
|
||||||
Config configuration.Configuration
|
Config configuration.Configuration
|
||||||
|
|
||||||
router *mux.Router
|
router *mux.Router
|
||||||
|
|
||||||
|
// driver maintains the app global storage driver instance.
|
||||||
|
driver storagedriver.StorageDriver
|
||||||
|
|
||||||
|
// services contains the main services instance for the application.
|
||||||
|
services *storage.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp takes a configuration and returns a configured app, ready to serve
|
// NewApp takes a configuration and returns a configured app, ready to serve
|
||||||
|
@ -29,11 +39,23 @@ func NewApp(configuration configuration.Configuration) *App {
|
||||||
|
|
||||||
// Register the handler dispatchers.
|
// Register the handler dispatchers.
|
||||||
app.register(routeNameImageManifest, imageManifestDispatcher)
|
app.register(routeNameImageManifest, imageManifestDispatcher)
|
||||||
app.register(routeNameBlob, layerDispatcher)
|
|
||||||
app.register(routeNameTags, tagsDispatcher)
|
app.register(routeNameTags, tagsDispatcher)
|
||||||
|
app.register(routeNameBlob, layerDispatcher)
|
||||||
app.register(routeNameBlobUpload, layerUploadDispatcher)
|
app.register(routeNameBlobUpload, layerUploadDispatcher)
|
||||||
app.register(routeNameBlobUploadResume, layerUploadDispatcher)
|
app.register(routeNameBlobUploadResume, layerUploadDispatcher)
|
||||||
|
|
||||||
|
driver, err := factory.Create(configuration.Storage.Type(), configuration.Storage.Parameters())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO(stevvooe): Move the creation of a service into a protected
|
||||||
|
// method, where this is created lazily. Its status can be queried via
|
||||||
|
// a health check.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.driver = driver
|
||||||
|
app.services = storage.NewServices(app.driver)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +86,22 @@ type dispatchFunc func(ctx *Context, r *http.Request) http.Handler
|
||||||
// TODO(stevvooe): dispatchers should probably have some validation error
|
// TODO(stevvooe): dispatchers should probably have some validation error
|
||||||
// chain with proper error reporting.
|
// chain with proper error reporting.
|
||||||
|
|
||||||
|
// singleStatusResponseWriter only allows the first status to be written to be
|
||||||
|
// the valid request status. The current use case of this class should be
|
||||||
|
// factored out.
|
||||||
|
type singleStatusResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ssrw *singleStatusResponseWriter) WriteHeader(status int) {
|
||||||
|
if ssrw.status != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ssrw.status = status
|
||||||
|
ssrw.ResponseWriter.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
// dispatcher returns a handler that constructs a request specific context and
|
// dispatcher returns a handler that constructs a request specific context and
|
||||||
// handler, using the dispatch factory function.
|
// handler, using the dispatch factory function.
|
||||||
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
||||||
|
@ -80,14 +118,17 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
||||||
context.log = log.WithField("name", context.Name)
|
context.log = log.WithField("name", context.Name)
|
||||||
handler := dispatch(context, r)
|
handler := dispatch(context, r)
|
||||||
|
|
||||||
|
ssrw := &singleStatusResponseWriter{ResponseWriter: w}
|
||||||
context.log.Infoln("handler", resolveHandlerName(r.Method, handler))
|
context.log.Infoln("handler", resolveHandlerName(r.Method, handler))
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(ssrw, r)
|
||||||
|
|
||||||
// Automated error response handling here. Handlers may return their
|
// Automated error response handling here. Handlers may return their
|
||||||
// own errors if they need different behavior (such as range errors
|
// own errors if they need different behavior (such as range errors
|
||||||
// for layer upload).
|
// for layer upload).
|
||||||
if len(context.Errors.Errors) > 0 {
|
if context.Errors.Len() > 0 {
|
||||||
|
if ssrw.status == 0 {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
serveJSON(w, context.Errors)
|
serveJSON(w, context.Errors)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import "github.com/Sirupsen/logrus"
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context should contain the request specific context for use in across
|
// Context should contain the request specific context for use in across
|
||||||
// handlers. Resources that don't need to be shared across handlers should not
|
// handlers. Resources that don't need to be shared across handlers should not
|
||||||
|
@ -20,11 +18,6 @@ type Context struct {
|
||||||
// handler *must not* start the response via http.ResponseWriter.
|
// handler *must not* start the response via http.ResponseWriter.
|
||||||
Errors Errors
|
Errors Errors
|
||||||
|
|
||||||
// TODO(stevvooe): Context would be a good place to create a
|
|
||||||
// representation of the "authorized resource". Perhaps, rather than
|
|
||||||
// having fields like "name", the context should be a set of parameters
|
|
||||||
// then we do routing from there.
|
|
||||||
|
|
||||||
// vars contains the extracted gorilla/mux variables that can be used for
|
// vars contains the extracted gorilla/mux variables that can be used for
|
||||||
// assignment.
|
// assignment.
|
||||||
vars map[string]string
|
vars map[string]string
|
||||||
|
|
43
errors.go
43
errors.go
|
@ -17,20 +17,14 @@ const (
|
||||||
|
|
||||||
// The following errors can happen during a layer upload.
|
// The following errors can happen during a layer upload.
|
||||||
|
|
||||||
// ErrorCodeInvalidChecksum is returned when uploading a layer if the
|
// ErrorCodeInvalidDigest is returned when uploading a layer if the
|
||||||
// provided checksum does not match the layer contents.
|
// provided digest does not match the layer contents.
|
||||||
ErrorCodeInvalidChecksum
|
ErrorCodeInvalidDigest
|
||||||
|
|
||||||
// ErrorCodeInvalidLength is returned when uploading a layer if the provided
|
// ErrorCodeInvalidLength is returned when uploading a layer if the provided
|
||||||
// length does not match the content length.
|
// length does not match the content length.
|
||||||
ErrorCodeInvalidLength
|
ErrorCodeInvalidLength
|
||||||
|
|
||||||
// ErrorCodeInvalidTarsum is returned when the provided tarsum does not
|
|
||||||
// match the computed tarsum of the contents.
|
|
||||||
ErrorCodeInvalidTarsum
|
|
||||||
|
|
||||||
// The following errors can happen during manifest upload.
|
|
||||||
|
|
||||||
// ErrorCodeInvalidName is returned when the name in the manifest does not
|
// ErrorCodeInvalidName is returned when the name in the manifest does not
|
||||||
// match the provided name.
|
// match the provided name.
|
||||||
ErrorCodeInvalidName
|
ErrorCodeInvalidName
|
||||||
|
@ -47,6 +41,9 @@ const (
|
||||||
// nonexistent layer.
|
// nonexistent layer.
|
||||||
ErrorCodeUnknownLayer
|
ErrorCodeUnknownLayer
|
||||||
|
|
||||||
|
// ErrorCodeUnknownLayerUpload is returned when an upload is accessed.
|
||||||
|
ErrorCodeUnknownLayerUpload
|
||||||
|
|
||||||
// ErrorCodeUntrustedSignature is returned when the manifest is signed by an
|
// ErrorCodeUntrustedSignature is returned when the manifest is signed by an
|
||||||
// untrusted source.
|
// untrusted source.
|
||||||
ErrorCodeUntrustedSignature
|
ErrorCodeUntrustedSignature
|
||||||
|
@ -54,25 +51,25 @@ const (
|
||||||
|
|
||||||
var errorCodeStrings = map[ErrorCode]string{
|
var errorCodeStrings = map[ErrorCode]string{
|
||||||
ErrorCodeUnknown: "UNKNOWN",
|
ErrorCodeUnknown: "UNKNOWN",
|
||||||
ErrorCodeInvalidChecksum: "INVALID_CHECKSUM",
|
ErrorCodeInvalidDigest: "INVALID_DIGEST",
|
||||||
ErrorCodeInvalidLength: "INVALID_LENGTH",
|
ErrorCodeInvalidLength: "INVALID_LENGTH",
|
||||||
ErrorCodeInvalidTarsum: "INVALID_TARSUM",
|
|
||||||
ErrorCodeInvalidName: "INVALID_NAME",
|
ErrorCodeInvalidName: "INVALID_NAME",
|
||||||
ErrorCodeInvalidTag: "INVALID_TAG",
|
ErrorCodeInvalidTag: "INVALID_TAG",
|
||||||
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
|
ErrorCodeUnverifiedManifest: "UNVERIFIED_MANIFEST",
|
||||||
ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
|
ErrorCodeUnknownLayer: "UNKNOWN_LAYER",
|
||||||
|
ErrorCodeUnknownLayerUpload: "UNKNOWN_LAYER_UPLOAD",
|
||||||
ErrorCodeUntrustedSignature: "UNTRUSTED_SIGNATURE",
|
ErrorCodeUntrustedSignature: "UNTRUSTED_SIGNATURE",
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorCodesMessages = map[ErrorCode]string{
|
var errorCodesMessages = map[ErrorCode]string{
|
||||||
ErrorCodeUnknown: "unknown error",
|
ErrorCodeUnknown: "unknown error",
|
||||||
ErrorCodeInvalidChecksum: "provided checksum did not match uploaded content",
|
ErrorCodeInvalidDigest: "provided digest did not match uploaded content",
|
||||||
ErrorCodeInvalidLength: "provided length did not match content length",
|
ErrorCodeInvalidLength: "provided length did not match content length",
|
||||||
ErrorCodeInvalidTarsum: "provided tarsum did not match binary content",
|
|
||||||
ErrorCodeInvalidName: "Manifest name did not match URI",
|
ErrorCodeInvalidName: "Manifest name did not match URI",
|
||||||
ErrorCodeInvalidTag: "Manifest tag did not match URI",
|
ErrorCodeInvalidTag: "Manifest tag did not match URI",
|
||||||
ErrorCodeUnverifiedManifest: "Manifest failed signature validation",
|
ErrorCodeUnverifiedManifest: "Manifest failed signature validation",
|
||||||
ErrorCodeUnknownLayer: "Referenced layer not available",
|
ErrorCodeUnknownLayer: "Referenced layer not available",
|
||||||
|
ErrorCodeUnknownLayerUpload: "cannot resume unknown layer upload",
|
||||||
ErrorCodeUntrustedSignature: "Manifest signed by untrusted source",
|
ErrorCodeUntrustedSignature: "Manifest signed by untrusted source",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +133,7 @@ func (ec *ErrorCode) UnmarshalText(text []byte) error {
|
||||||
|
|
||||||
// Error provides a wrapper around ErrorCode with extra Details provided.
|
// Error provides a wrapper around ErrorCode with extra Details provided.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Code ErrorCode `json:"code,omitempty"`
|
Code ErrorCode `json:"code"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Detail interface{} `json:"detail,omitempty"`
|
Detail interface{} `json:"detail,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -144,7 +141,7 @@ type Error struct {
|
||||||
// Error returns a human readable representation of the error.
|
// Error returns a human readable representation of the error.
|
||||||
func (e Error) Error() string {
|
func (e Error) Error() string {
|
||||||
return fmt.Sprintf("%s: %s",
|
return fmt.Sprintf("%s: %s",
|
||||||
strings.Title(strings.Replace(e.Code.String(), "_", " ", -1)),
|
strings.ToLower(strings.Replace(e.Code.String(), "_", " ", -1)),
|
||||||
e.Message)
|
e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,6 +164,10 @@ func (errs *Errors) Push(code ErrorCode, details ...interface{}) {
|
||||||
detail = details[0]
|
detail = details[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err, ok := detail.(error); ok {
|
||||||
|
detail = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
errs.PushErr(Error{
|
errs.PushErr(Error{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: code.Message(),
|
Message: code.Message(),
|
||||||
|
@ -180,7 +181,7 @@ func (errs *Errors) PushErr(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (errs *Errors) Error() string {
|
func (errs *Errors) Error() string {
|
||||||
switch len(errs.Errors) {
|
switch errs.Len() {
|
||||||
case 0:
|
case 0:
|
||||||
return "<nil>"
|
return "<nil>"
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -194,6 +195,16 @@ func (errs *Errors) Error() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear clears the errors.
|
||||||
|
func (errs *Errors) Clear() {
|
||||||
|
errs.Errors = errs.Errors[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the current number of errors.
|
||||||
|
func (errs *Errors) Len() int {
|
||||||
|
return len(errs.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
// DetailUnknownLayer provides detail for unknown layer errors, returned by
|
// DetailUnknownLayer provides detail for unknown layer errors, returned by
|
||||||
// image manifest push for layers that are not yet transferred. This intended
|
// image manifest push for layers that are not yet transferred. This intended
|
||||||
// to only be used on the backend to return detail for this specific error.
|
// to only be used on the backend to return detail for this specific error.
|
||||||
|
|
|
@ -56,7 +56,7 @@ func TestErrorCodes(t *testing.T) {
|
||||||
func TestErrorsManagement(t *testing.T) {
|
func TestErrorsManagement(t *testing.T) {
|
||||||
var errs Errors
|
var errs Errors
|
||||||
|
|
||||||
errs.Push(ErrorCodeInvalidChecksum)
|
errs.Push(ErrorCodeInvalidDigest)
|
||||||
|
|
||||||
var detail DetailUnknownLayer
|
var detail DetailUnknownLayer
|
||||||
detail.Unknown.BlobSum = "sometestblobsumdoesntmatter"
|
detail.Unknown.BlobSum = "sometestblobsumdoesntmatter"
|
||||||
|
@ -69,7 +69,20 @@ func TestErrorsManagement(t *testing.T) {
|
||||||
t.Fatalf("error marashaling errors: %v", err)
|
t.Fatalf("error marashaling errors: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedJSON := "{\"errors\":[{\"code\":\"INVALID_CHECKSUM\",\"message\":\"provided checksum did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"Referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}"
|
expectedJSON := "{\"errors\":[{\"code\":\"INVALID_DIGEST\",\"message\":\"provided digest did not match uploaded content\"},{\"code\":\"UNKNOWN_LAYER\",\"message\":\"Referenced layer not available\",\"detail\":{\"unknown\":{\"blobSum\":\"sometestblobsumdoesntmatter\"}}}]}"
|
||||||
|
|
||||||
|
if string(p) != expectedJSON {
|
||||||
|
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
errs.Clear()
|
||||||
|
errs.Push(ErrorCodeUnknown)
|
||||||
|
expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}"
|
||||||
|
p, err = json.Marshal(errs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error marashaling errors: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if string(p) != expectedJSON {
|
if string(p) != expectedJSON {
|
||||||
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
|
t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON)
|
||||||
|
|
21
helpers.go
21
helpers.go
|
@ -2,7 +2,10 @@ package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serveJSON marshals v and sets the content-type header to
|
// serveJSON marshals v and sets the content-type header to
|
||||||
|
@ -18,3 +21,21 @@ func serveJSON(w http.ResponseWriter, v interface{}) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// closeResources closes all the provided resources after running the target
|
||||||
|
// handler.
|
||||||
|
func closeResources(handler http.Handler, closers ...io.Closer) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
for _, closer := range closers {
|
||||||
|
defer closer.Close()
|
||||||
|
}
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// clondedRoute returns a clone of the named route from the router.
|
||||||
|
func clonedRoute(router *mux.Router, name string) *mux.Route {
|
||||||
|
route := new(mux.Route)
|
||||||
|
*route = *router.GetRoute(name) // clone the route
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
54
layer.go
54
layer.go
|
@ -3,17 +3,28 @@ package registry
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/digest"
|
||||||
|
"github.com/docker/docker-registry/storage"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// layerDispatcher uses the request context to build a layerHandler.
|
// layerDispatcher uses the request context to build a layerHandler.
|
||||||
func layerDispatcher(ctx *Context, r *http.Request) http.Handler {
|
func layerDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||||
layerHandler := &layerHandler{
|
dgst, err := digest.ParseDigest(ctx.vars["digest"])
|
||||||
Context: ctx,
|
|
||||||
TarSum: ctx.vars["tarsum"],
|
if err != nil {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx.Errors.Push(ErrorCodeInvalidDigest, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
layerHandler.log = layerHandler.log.WithField("tarsum", layerHandler.TarSum)
|
layerHandler := &layerHandler{
|
||||||
|
Context: ctx,
|
||||||
|
Digest: dgst,
|
||||||
|
}
|
||||||
|
|
||||||
|
layerHandler.log = layerHandler.log.WithField("digest", dgst)
|
||||||
|
|
||||||
return handlers.MethodHandler{
|
return handlers.MethodHandler{
|
||||||
"GET": http.HandlerFunc(layerHandler.GetLayer),
|
"GET": http.HandlerFunc(layerHandler.GetLayer),
|
||||||
|
@ -25,11 +36,44 @@ func layerDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||||
type layerHandler struct {
|
type layerHandler struct {
|
||||||
*Context
|
*Context
|
||||||
|
|
||||||
TarSum string
|
Digest digest.Digest
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLayer fetches the binary data from backend storage returns it in the
|
// GetLayer fetches the binary data from backend storage returns it in the
|
||||||
// response.
|
// response.
|
||||||
func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
|
func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
layers := lh.services.Layers()
|
||||||
|
|
||||||
|
layer, err := layers.Fetch(lh.Name, lh.Digest)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case storage.ErrLayerUnknown:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
lh.Errors.Push(ErrorCodeUnknownLayer,
|
||||||
|
map[string]interface{}{
|
||||||
|
"unknown": FSLayer{BlobSum: lh.Digest},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
lh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer layer.Close()
|
||||||
|
|
||||||
|
http.ServeContent(w, r, layer.Digest().String(), layer.CreatedAt(), layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLayerURL(router *mux.Router, r *http.Request, layer storage.Layer) (string, error) {
|
||||||
|
route := clonedRoute(router, routeNameBlob)
|
||||||
|
|
||||||
|
layerURL, err := route.Schemes(r.URL.Scheme).Host(r.Host).
|
||||||
|
URL("name", layer.Name(),
|
||||||
|
"digest", layer.Digest().String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return layerURL.String(), nil
|
||||||
}
|
}
|
||||||
|
|
189
layerupload.go
189
layerupload.go
|
@ -1,64 +1,225 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker-registry/digest"
|
||||||
|
"github.com/docker/docker-registry/storage"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// layerUploadDispatcher constructs and returns the layer upload handler for
|
// layerUploadDispatcher constructs and returns the layer upload handler for
|
||||||
// the given request context.
|
// the given request context.
|
||||||
func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
|
func layerUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||||
layerUploadHandler := &layerUploadHandler{
|
luh := &layerUploadHandler{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
TarSum: ctx.vars["tarsum"],
|
|
||||||
UUID: ctx.vars["uuid"],
|
UUID: ctx.vars["uuid"],
|
||||||
}
|
}
|
||||||
|
|
||||||
layerUploadHandler.log = layerUploadHandler.log.WithField("tarsum", layerUploadHandler.TarSum)
|
handler := http.Handler(handlers.MethodHandler{
|
||||||
|
"POST": http.HandlerFunc(luh.StartLayerUpload),
|
||||||
|
"GET": http.HandlerFunc(luh.GetUploadStatus),
|
||||||
|
"HEAD": http.HandlerFunc(luh.GetUploadStatus),
|
||||||
|
"PUT": http.HandlerFunc(luh.PutLayerChunk),
|
||||||
|
"DELETE": http.HandlerFunc(luh.CancelLayerUpload),
|
||||||
|
})
|
||||||
|
|
||||||
if layerUploadHandler.UUID != "" {
|
if luh.UUID != "" {
|
||||||
layerUploadHandler.log = layerUploadHandler.log.WithField("uuid", layerUploadHandler.UUID)
|
luh.log = luh.log.WithField("uuid", luh.UUID)
|
||||||
|
|
||||||
|
layers := ctx.services.Layers()
|
||||||
|
upload, err := layers.Resume(luh.UUID)
|
||||||
|
|
||||||
|
if err != nil && err != storage.ErrLayerUploadUnknown {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logrus.Infof("error resolving upload: %v", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return handlers.MethodHandler{
|
luh.Upload = upload
|
||||||
"POST": http.HandlerFunc(layerUploadHandler.StartLayerUpload),
|
handler = closeResources(handler, luh.Upload)
|
||||||
"GET": http.HandlerFunc(layerUploadHandler.GetUploadStatus),
|
|
||||||
"HEAD": http.HandlerFunc(layerUploadHandler.GetUploadStatus),
|
|
||||||
"PUT": http.HandlerFunc(layerUploadHandler.PutLayerChunk),
|
|
||||||
"DELETE": http.HandlerFunc(layerUploadHandler.CancelLayerUpload),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// layerUploadHandler handles the http layer upload process.
|
// layerUploadHandler handles the http layer upload process.
|
||||||
type layerUploadHandler struct {
|
type layerUploadHandler struct {
|
||||||
*Context
|
*Context
|
||||||
|
|
||||||
// TarSum is the unique identifier of the layer being uploaded.
|
|
||||||
TarSum string
|
|
||||||
|
|
||||||
// UUID identifies the upload instance for the current request.
|
// UUID identifies the upload instance for the current request.
|
||||||
UUID string
|
UUID string
|
||||||
|
|
||||||
|
Upload storage.LayerUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartLayerUpload begins the layer upload process and allocates a server-
|
// StartLayerUpload begins the layer upload process and allocates a server-
|
||||||
// side upload session.
|
// side upload session.
|
||||||
func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.Request) {
|
func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
layers := luh.services.Layers()
|
||||||
|
upload, err := layers.Upload(luh.Name)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
luh.Upload = upload
|
||||||
|
defer luh.Upload.Close()
|
||||||
|
|
||||||
|
if err := luh.layerUploadResponse(w, r); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUploadStatus returns the status of a given upload, identified by uuid.
|
// GetUploadStatus returns the status of a given upload, identified by uuid.
|
||||||
func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
|
func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if luh.Upload == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
luh.Errors.Push(ErrorCodeUnknownLayerUpload)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := luh.layerUploadResponse(w, r); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutLayerChunk receives a layer chunk during the layer upload process,
|
// PutLayerChunk receives a layer chunk during the layer upload process,
|
||||||
// possible completing the upload with a checksum and length.
|
// possible completing the upload with a checksum and length.
|
||||||
func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) {
|
func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if luh.Upload == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
luh.Errors.Push(ErrorCodeUnknownLayerUpload)
|
||||||
|
}
|
||||||
|
|
||||||
|
var finished bool
|
||||||
|
|
||||||
|
// TODO(stevvooe): This is woefully incomplete. Missing stuff:
|
||||||
|
//
|
||||||
|
// 1. Extract information from range header, if present.
|
||||||
|
// 2. Check offset of current layer.
|
||||||
|
// 3. Emit correct error responses.
|
||||||
|
|
||||||
|
// Read in the chunk
|
||||||
|
io.Copy(luh.Upload, r.Body)
|
||||||
|
|
||||||
|
if err := luh.maybeCompleteUpload(w, r); err != nil {
|
||||||
|
if err != errNotReadyToComplete {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := luh.layerUploadResponse(w, r); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError) // Error conditions here?
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if finished {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelLayerUpload cancels an in-progress upload of a layer.
|
// CancelLayerUpload cancels an in-progress upload of a layer.
|
||||||
func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) {
|
func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if luh.Upload == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
luh.Errors.Push(ErrorCodeUnknownLayerUpload)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (luh *layerUploadHandler) layerUploadResponse(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
uploadURL, err := buildLayerUploadURL(luh.router, r, luh.Upload)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Infof("error building upload url: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Location", uploadURL)
|
||||||
|
w.Header().Set("Content-Length", "0")
|
||||||
|
w.Header().Set("Range", fmt.Sprintf("0-%d", luh.Upload.Offset()))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNotReadyToComplete = fmt.Errorf("not ready to complete upload")
|
||||||
|
|
||||||
|
// maybeCompleteUpload tries to complete the upload if the correct parameters
|
||||||
|
// are available. Returns errNotReadyToComplete if not ready to complete.
|
||||||
|
func (luh *layerUploadHandler) maybeCompleteUpload(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
// If we get a digest and length, we can finish the upload.
|
||||||
|
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
|
||||||
|
sizeStr := r.FormValue("length")
|
||||||
|
|
||||||
|
if dgstStr == "" || sizeStr == "" {
|
||||||
|
return errNotReadyToComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := digest.ParseDigest(dgstStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
size, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
luh.completeUpload(w, r, size, dgst)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeUpload finishes out the upload with the correct response.
|
||||||
|
func (luh *layerUploadHandler) completeUpload(w http.ResponseWriter, r *http.Request, size int64, dgst digest.Digest) {
|
||||||
|
layer, err := luh.Upload.Finish(size, dgst)
|
||||||
|
if err != nil {
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
layerURL, err := buildLayerURL(luh.router, r, layer)
|
||||||
|
if err != nil {
|
||||||
|
luh.Errors.Push(ErrorCodeUnknown, err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Location", layerURL)
|
||||||
|
w.Header().Set("Content-Length", "0")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLayerUploadURL(router *mux.Router, r *http.Request, upload storage.LayerUpload) (string, error) {
|
||||||
|
route := clonedRoute(router, routeNameBlobUploadResume)
|
||||||
|
|
||||||
|
uploadURL, err := route.Schemes(r.URL.Scheme).Host(r.Host).
|
||||||
|
URL("name", upload.Name(), "uuid", upload.UUID())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadURL.String(), nil
|
||||||
|
}
|
||||||
|
|
163
storage/filereader.go
Normal file
163
storage/filereader.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/storagedriver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// remoteFileReader provides a read seeker interface to files stored in
|
||||||
|
// storagedriver. Used to implement part of layer interface and will be used
|
||||||
|
// to implement read side of LayerUpload.
|
||||||
|
type fileReader struct {
|
||||||
|
driver storagedriver.StorageDriver
|
||||||
|
|
||||||
|
// identifying fields
|
||||||
|
path string
|
||||||
|
size int64 // size is the total layer size, must be set.
|
||||||
|
|
||||||
|
// mutable fields
|
||||||
|
rc io.ReadCloser // remote read closer
|
||||||
|
brd *bufio.Reader // internal buffered io
|
||||||
|
offset int64 // offset is the current read offset
|
||||||
|
err error // terminal error, if set, reader is closed
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileReader(driver storagedriver.StorageDriver, path string) (*fileReader, error) {
|
||||||
|
// Grab the size of the layer file, ensuring existence.
|
||||||
|
size, err := driver.CurrentSize(path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fileReader{
|
||||||
|
driver: driver,
|
||||||
|
path: path,
|
||||||
|
size: int64(size),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fr *fileReader) Read(p []byte) (n int, err error) {
|
||||||
|
if fr.err != nil {
|
||||||
|
return 0, fr.err
|
||||||
|
}
|
||||||
|
|
||||||
|
rd, err := fr.reader()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = rd.Read(p)
|
||||||
|
fr.offset += int64(n)
|
||||||
|
|
||||||
|
// Simulate io.EOR error if we reach filesize.
|
||||||
|
if err == nil && fr.offset >= fr.size {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fr *fileReader) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if fr.err != nil {
|
||||||
|
return 0, fr.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
newOffset := fr.offset
|
||||||
|
|
||||||
|
switch whence {
|
||||||
|
case os.SEEK_CUR:
|
||||||
|
newOffset += int64(offset)
|
||||||
|
case os.SEEK_END:
|
||||||
|
newOffset = fr.size + int64(offset)
|
||||||
|
case os.SEEK_SET:
|
||||||
|
newOffset = int64(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if newOffset < 0 {
|
||||||
|
err = fmt.Errorf("cannot seek to negative position")
|
||||||
|
} else if newOffset > fr.size {
|
||||||
|
err = fmt.Errorf("cannot seek passed end of file")
|
||||||
|
} else {
|
||||||
|
if fr.offset != newOffset {
|
||||||
|
fr.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// No problems, set the offset.
|
||||||
|
fr.offset = newOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
return fr.offset, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the layer. Should be called when the resource is no longer needed.
|
||||||
|
func (fr *fileReader) Close() error {
|
||||||
|
if fr.err != nil {
|
||||||
|
return fr.err
|
||||||
|
}
|
||||||
|
|
||||||
|
fr.err = ErrLayerClosed
|
||||||
|
|
||||||
|
// close and release reader chain
|
||||||
|
if fr.rc != nil {
|
||||||
|
fr.rc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
fr.rc = nil
|
||||||
|
fr.brd = nil
|
||||||
|
|
||||||
|
return fr.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader prepares the current reader at the lrs offset, ensuring its buffered
|
||||||
|
// and ready to go.
|
||||||
|
func (fr *fileReader) reader() (io.Reader, error) {
|
||||||
|
if fr.err != nil {
|
||||||
|
return nil, fr.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fr.rc != nil {
|
||||||
|
return fr.brd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a reader, open one up.
|
||||||
|
rc, err := fr.driver.ReadStream(fr.path, uint64(fr.offset))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fr.rc = rc
|
||||||
|
|
||||||
|
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.NewReader(fr.rc)
|
||||||
|
} else {
|
||||||
|
fr.brd.Reset(fr.rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fr.brd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetReader resets the reader, forcing the read method to open up a new
|
||||||
|
// connection and rebuild the buffered reader. This should be called when the
|
||||||
|
// offset and the reader will become out of sync, such as during a seek
|
||||||
|
// operation.
|
||||||
|
func (fr *fileReader) reset() {
|
||||||
|
if fr.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if fr.rc != nil {
|
||||||
|
fr.rc.Close()
|
||||||
|
fr.rc = nil
|
||||||
|
}
|
||||||
|
}
|
158
storage/filereader_test.go
Normal file
158
storage/filereader_test.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"io"
|
||||||
|
mrand "math/rand"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/digest"
|
||||||
|
|
||||||
|
"github.com/docker/docker-registry/storagedriver/inmemory"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimpleRead(t *testing.T) {
|
||||||
|
content := make([]byte, 1<<20)
|
||||||
|
n, err := rand.Read(content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error building random data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != len(content) {
|
||||||
|
t.Fatalf("random read did't fill buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
dgst, err := digest.FromReader(bytes.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error digesting random content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver := inmemory.New()
|
||||||
|
path := "/random"
|
||||||
|
|
||||||
|
if err := driver.PutContent(path, content); err != nil {
|
||||||
|
t.Fatalf("error putting patterned content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fr, err := newFileReader(driver, path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error allocating file reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := digest.NewDigestVerifier(dgst)
|
||||||
|
io.Copy(verifier, fr)
|
||||||
|
|
||||||
|
if !verifier.Verified() {
|
||||||
|
t.Fatalf("unable to verify read data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileReaderSeek(t *testing.T) {
|
||||||
|
driver := inmemory.New()
|
||||||
|
pattern := "01234567890ab" // prime length block
|
||||||
|
repititions := 1024
|
||||||
|
path := "/patterned"
|
||||||
|
content := bytes.Repeat([]byte(pattern), repititions)
|
||||||
|
|
||||||
|
if err := driver.PutContent(path, content); err != nil {
|
||||||
|
t.Fatalf("error putting patterned content: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fr, err := newFileReader(driver, path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating file reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek all over the place, in blocks of pattern size and make sure we get
|
||||||
|
// the right data.
|
||||||
|
for _, repitition := range mrand.Perm(repititions - 1) {
|
||||||
|
targetOffset := int64(len(pattern) * repitition)
|
||||||
|
// Seek to a multiple of pattern size and read pattern size bytes
|
||||||
|
offset, err := fr.Seek(targetOffset, os.SEEK_SET)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error seeking: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset != targetOffset {
|
||||||
|
t.Fatalf("did not seek to correct offset: %d != %d", offset, targetOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := make([]byte, len(pattern))
|
||||||
|
|
||||||
|
n, err := fr.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error reading pattern: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != len(pattern) {
|
||||||
|
t.Fatalf("incorrect read length: %d != %d", n, len(pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(p) != pattern {
|
||||||
|
t.Fatalf("incorrect read content: %q != %q", p, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check offset
|
||||||
|
current, err := fr.Seek(0, os.SEEK_CUR)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error checking current offset: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if current != targetOffset+int64(len(pattern)) {
|
||||||
|
t.Fatalf("unexpected offset after read: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := fr.Seek(0, os.SEEK_SET)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error seeking to start: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if start != 0 {
|
||||||
|
t.Fatalf("expected to seek to start: %v != 0", start)
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := fr.Seek(0, os.SEEK_END)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error checking current offset: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if end != int64(len(content)) {
|
||||||
|
t.Fatalf("expected to seek to end: %v != %v", end, len(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Seek past end and before start, ensure error.
|
||||||
|
|
||||||
|
// seek before start
|
||||||
|
before, err := fr.Seek(-1, os.SEEK_SET)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("error expected, returned offset=%v", before)
|
||||||
|
}
|
||||||
|
|
||||||
|
after, err := fr.Seek(int64(len(content)+1), os.SEEK_END)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("error expected, returned offset=%v", after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLayerReadErrors covers the various error return type for different
|
||||||
|
// conditions that can arise when reading a layer.
|
||||||
|
func TestFileReaderErrors(t *testing.T) {
|
||||||
|
// TODO(stevvooe): We need to cover error return types, driven by the
|
||||||
|
// errors returned via the HTTP API. For now, here is a incomplete list:
|
||||||
|
//
|
||||||
|
// 1. Layer Not Found: returned when layer is not found or access is
|
||||||
|
// denied.
|
||||||
|
// 2. Layer Unavailable: returned when link references are unresolved,
|
||||||
|
// but layer is known to the registry.
|
||||||
|
// 3. Layer Invalid: This may more split into more errors, but should be
|
||||||
|
// returned when name or tarsum does not reference a valid error. We
|
||||||
|
// may also need something to communication layer verification errors
|
||||||
|
// for the inline tarsum check.
|
||||||
|
// 4. Timeout: timeouts to backend. Need to better understand these
|
||||||
|
// failure cases and how the storage driver propagates these errors
|
||||||
|
// up the stack.
|
||||||
|
}
|
|
@ -87,4 +87,8 @@ var (
|
||||||
|
|
||||||
// ErrLayerInvalidLength returned when length check fails.
|
// ErrLayerInvalidLength returned when length check fails.
|
||||||
ErrLayerInvalidLength = fmt.Errorf("invalid layer length")
|
ErrLayerInvalidLength = fmt.Errorf("invalid layer length")
|
||||||
|
|
||||||
|
// ErrLayerClosed returned when an operation is attempted on a closed
|
||||||
|
// Layer or LayerUpload.
|
||||||
|
ErrLayerClosed = fmt.Errorf("layer closed")
|
||||||
)
|
)
|
||||||
|
|
|
@ -241,31 +241,6 @@ func TestSimpleLayerRead(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLayerReaderSeek(t *testing.T) {
|
|
||||||
// TODO(stevvooe): Ensure that all relative seeks work as advertised.
|
|
||||||
// Readers must close and re-open on command. This is important to support
|
|
||||||
// resumable and concurrent downloads via HTTP range requests.
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLayerReadErrors covers the various error return type for different
|
|
||||||
// conditions that can arise when reading a layer.
|
|
||||||
func TestLayerReadErrors(t *testing.T) {
|
|
||||||
// TODO(stevvooe): We need to cover error return types, driven by the
|
|
||||||
// errors returned via the HTTP API. For now, here is a incomplete list:
|
|
||||||
//
|
|
||||||
// 1. Layer Not Found: returned when layer is not found or access is
|
|
||||||
// denied.
|
|
||||||
// 2. Layer Unavailable: returned when link references are unresolved,
|
|
||||||
// but layer is known to the registry.
|
|
||||||
// 3. Layer Invalid: This may more split into more errors, but should be
|
|
||||||
// returned when name or tarsum does not reference a valid error. We
|
|
||||||
// may also need something to communication layer verification errors
|
|
||||||
// for the inline tarsum check.
|
|
||||||
// 4. Timeout: timeouts to backend. Need to better understand these
|
|
||||||
// failure cases and how the storage driver propagates these errors
|
|
||||||
// up the stack.
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeRandomLayer creates a random layer under name and tarSum using driver
|
// writeRandomLayer creates a random layer under name and tarSum using driver
|
||||||
// and pathMapper. An io.ReadSeeker with the data is returned, along with the
|
// and pathMapper. An io.ReadSeeker with the data is returned, along with the
|
||||||
// sha256 hex digest.
|
// sha256 hex digest.
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker-registry/digest"
|
"github.com/docker/docker-registry/digest"
|
||||||
|
@ -13,22 +9,11 @@ import (
|
||||||
// layerReadSeeker implements Layer and provides facilities for reading and
|
// layerReadSeeker implements Layer and provides facilities for reading and
|
||||||
// seeking.
|
// seeking.
|
||||||
type layerReader struct {
|
type layerReader struct {
|
||||||
layerStore *layerStore
|
fileReader
|
||||||
rc io.ReadCloser
|
|
||||||
brd *bufio.Reader
|
|
||||||
|
|
||||||
name string // repo name of this layer
|
name string // repo name of this layer
|
||||||
digest digest.Digest
|
digest digest.Digest
|
||||||
path string
|
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
|
|
||||||
// offset is the current read offset
|
|
||||||
offset int64
|
|
||||||
|
|
||||||
// size is the total layer size, if available.
|
|
||||||
size int64
|
|
||||||
|
|
||||||
closedErr error // terminal error, if set, reader is closed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Layer = &layerReader{}
|
var _ Layer = &layerReader{}
|
||||||
|
@ -44,131 +29,3 @@ func (lrs *layerReader) Digest() digest.Digest {
|
||||||
func (lrs *layerReader) CreatedAt() time.Time {
|
func (lrs *layerReader) CreatedAt() time.Time {
|
||||||
return lrs.createdAt
|
return lrs.createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lrs *layerReader) Read(p []byte) (n int, err error) {
|
|
||||||
if err := lrs.closed(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rd, err := lrs.reader()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = rd.Read(p)
|
|
||||||
lrs.offset += int64(n)
|
|
||||||
|
|
||||||
// Simulate io.EOR error if we reach filesize.
|
|
||||||
if err == nil && lrs.offset >= lrs.size {
|
|
||||||
err = io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(stevvooe): More error checking is required here. If the reader
|
|
||||||
// times out for some reason, we should reset the reader so we re-open the
|
|
||||||
// connection.
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lrs *layerReader) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
if err := lrs.closed(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
newOffset := lrs.offset
|
|
||||||
|
|
||||||
switch whence {
|
|
||||||
case os.SEEK_CUR:
|
|
||||||
newOffset += int64(whence)
|
|
||||||
case os.SEEK_END:
|
|
||||||
newOffset = lrs.size + int64(whence)
|
|
||||||
case os.SEEK_SET:
|
|
||||||
newOffset = int64(whence)
|
|
||||||
}
|
|
||||||
|
|
||||||
if newOffset < 0 {
|
|
||||||
err = fmt.Errorf("cannot seek to negative position")
|
|
||||||
} else if newOffset >= lrs.size {
|
|
||||||
err = fmt.Errorf("cannot seek passed end of layer")
|
|
||||||
} else {
|
|
||||||
if lrs.offset != newOffset {
|
|
||||||
lrs.resetReader()
|
|
||||||
}
|
|
||||||
|
|
||||||
// No problems, set the offset.
|
|
||||||
lrs.offset = newOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
return lrs.offset, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the layer. Should be called when the resource is no longer needed.
|
|
||||||
func (lrs *layerReader) Close() error {
|
|
||||||
if lrs.closedErr != nil {
|
|
||||||
return lrs.closedErr
|
|
||||||
}
|
|
||||||
// TODO(sday): Must export this error.
|
|
||||||
lrs.closedErr = fmt.Errorf("layer closed")
|
|
||||||
|
|
||||||
// close and release reader chain
|
|
||||||
if lrs.rc != nil {
|
|
||||||
lrs.rc.Close()
|
|
||||||
lrs.rc = nil
|
|
||||||
}
|
|
||||||
lrs.brd = nil
|
|
||||||
|
|
||||||
return lrs.closedErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// reader prepares the current reader at the lrs offset, ensuring its buffered
|
|
||||||
// and ready to go.
|
|
||||||
func (lrs *layerReader) reader() (io.Reader, error) {
|
|
||||||
if err := lrs.closed(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if lrs.rc != nil {
|
|
||||||
return lrs.brd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we don't have a reader, open one up.
|
|
||||||
rc, err := lrs.layerStore.driver.ReadStream(lrs.path, uint64(lrs.offset))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lrs.rc = rc
|
|
||||||
|
|
||||||
if lrs.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.
|
|
||||||
lrs.brd = bufio.NewReader(lrs.rc)
|
|
||||||
} else {
|
|
||||||
lrs.brd.Reset(lrs.rc)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lrs.brd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetReader resets the reader, forcing the read method to open up a new
|
|
||||||
// connection and rebuild the buffered reader. This should be called when the
|
|
||||||
// offset and the reader will become out of sync, such as during a seek
|
|
||||||
// operation.
|
|
||||||
func (lrs *layerReader) resetReader() {
|
|
||||||
if err := lrs.closed(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if lrs.rc != nil {
|
|
||||||
lrs.rc.Close()
|
|
||||||
lrs.rc = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lrs *layerReader) closed() error {
|
|
||||||
return lrs.closedErr
|
|
||||||
}
|
|
||||||
|
|
|
@ -57,33 +57,26 @@ func (ls *layerStore) Fetch(name string, digest digest.Digest) (Layer, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab the size of the layer file, ensuring that it exists, among other
|
fr, err := newFileReader(ls.driver, p)
|
||||||
// things.
|
|
||||||
size, err := ls.driver.CurrentSize(p)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(stevvooe): Handle blob/path does not exist here.
|
switch err := err.(type) {
|
||||||
// TODO(stevvooe): Get a better understanding of the error cases here
|
case storagedriver.PathNotFoundError, *storagedriver.PathNotFoundError:
|
||||||
// that don't stem from unknown path.
|
return nil, ErrLayerUnknown
|
||||||
|
default:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build the layer reader and return to the client.
|
return &layerReader{
|
||||||
layer := &layerReader{
|
fileReader: *fr,
|
||||||
layerStore: ls,
|
|
||||||
path: p,
|
|
||||||
name: name,
|
name: name,
|
||||||
digest: digest,
|
digest: digest,
|
||||||
|
|
||||||
// TODO(stevvooe): Storage backend does not support modification time
|
// TODO(stevvooe): Storage backend does not support modification time
|
||||||
// queries yet. Layers "never" change, so just return the zero value.
|
// queries yet. Layers "never" change, so just return the zero value
|
||||||
createdAt: time.Time{},
|
// plus a nano-second.
|
||||||
|
createdAt: (time.Time{}).Add(time.Nanosecond),
|
||||||
offset: 0,
|
}, nil
|
||||||
size: int64(size),
|
|
||||||
}
|
|
||||||
|
|
||||||
return layer, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload begins a layer upload, returning a handle. If the layer upload
|
// Upload begins a layer upload, returning a handle. If the layer upload
|
||||||
|
|
|
@ -429,6 +429,10 @@ func (llufs *localFSLayerUploadStore) New(name string) (LayerUploadState, error)
|
||||||
return lus, err
|
return lus, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := llufs.SaveState(lus); err != nil {
|
||||||
|
return lus, err
|
||||||
|
}
|
||||||
|
|
||||||
return lus, nil
|
return lus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ type Services struct {
|
||||||
// NewServices creates a new Services object to access docker objects stored
|
// NewServices creates a new Services object to access docker objects stored
|
||||||
// in the underlying driver.
|
// in the underlying driver.
|
||||||
func NewServices(driver storagedriver.StorageDriver) *Services {
|
func NewServices(driver storagedriver.StorageDriver) *Services {
|
||||||
|
|
||||||
layerUploadStore, err := newTemporaryLocalFSLayerUploadStore()
|
layerUploadStore, err := newTemporaryLocalFSLayerUploadStore()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -40,5 +39,5 @@ func NewServices(driver storagedriver.StorageDriver) *Services {
|
||||||
// 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 (ss *Services) Layers() LayerService {
|
func (ss *Services) Layers() LayerService {
|
||||||
return &layerStore{driver: ss.driver, pathMapper: ss.pathMapper}
|
return &layerStore{driver: ss.driver, pathMapper: ss.pathMapper, uploadStore: ss.layerUploadStore}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue