diff --git a/app.go b/app.go new file mode 100644 index 00000000..63635250 --- /dev/null +++ b/app.go @@ -0,0 +1,94 @@ +package registry + +import ( + "net/http" + + "github.com/docker/docker-registry/configuration" + + log "github.com/Sirupsen/logrus" + "github.com/gorilla/mux" +) + +// App is a global registry application object. Shared resources can be placed +// on this object that will be accessible from all requests. Any writable +// fields should be protected. +type App struct { + Config configuration.Configuration + + router *mux.Router +} + +// NewApp takes a configuration and returns a configured app, ready to serve +// requests. The app only implements ServeHTTP and can be wrapped in other +// handlers accordingly. +func NewApp(configuration configuration.Configuration) *App { + app := &App{ + Config: configuration, + router: v2APIRouter(), + } + + // Register the handler dispatchers. + app.register(routeNameImageManifest, imageManifestDispatcher) + app.register(routeNameLayer, layerDispatcher) + app.register(routeNameTags, tagsDispatcher) + app.register(routeNameLayerUpload, layerUploadDispatcher) + app.register(routeNameLayerUploadResume, layerUploadDispatcher) + + return app +} + +func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { + app.router.ServeHTTP(w, r) +} + +// register a handler with the application, by route name. The handler will be +// passed through the application filters and context will be constructed at +// request time. +func (app *App) register(routeName string, dispatch dispatchFunc) { + + // TODO(stevvooe): This odd dispatcher/route registration is by-product of + // some limitations in the gorilla/mux router. We are using it to keep + // routing consistent between the client and server, but we may want to + // replace it with manual routing and structure-based dispatch for better + // control over the request execution. + + app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch)) +} + +// dispatchFunc takes a context and request and returns a constructed handler +// for the route. The dispatcher will use this to dynamically create request +// specific handlers for each endpoint without creating a new router for each +// request. +type dispatchFunc func(ctx *Context, r *http.Request) http.Handler + +// TODO(stevvooe): dispatchers should probably have some validation error +// chain with proper error reporting. + +// dispatcher returns a handler that constructs a request specific context and +// handler, using the dispatch factory function. +func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + context := &Context{ + App: app, + Name: vars["name"], + } + + // Store vars for underlying handlers. + context.vars = vars + + context.log = log.WithField("name", context.Name) + handler := dispatch(context, r) + + context.log.Infoln("handler", resolveHandlerName(r.Method, handler)) + handler.ServeHTTP(w, r) + + // Automated error response handling here. Handlers may return their + // own errors if they need different behavior (such as range errors + // for layer upload). + if len(context.Errors.Errors) > 0 { + w.WriteHeader(http.StatusBadRequest) + serveJSON(w, context.Errors) + } + }) +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 00000000..43b001ec --- /dev/null +++ b/app_test.go @@ -0,0 +1,127 @@ +package registry + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/docker/docker-registry/configuration" +) + +// TestAppDispatcher builds an application with a test dispatcher and ensures +// that requests are properly dispatched and the handlers are constructed. +// This only tests the dispatch mechanism. The underlying dispatchers must be +// tested individually. +func TestAppDispatcher(t *testing.T) { + app := &App{ + Config: configuration.Configuration{}, + router: v2APIRouter(), + } + server := httptest.NewServer(app) + router := v2APIRouter() + + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("error parsing server url: %v", err) + } + + varCheckingDispatcher := func(expectedVars map[string]string) dispatchFunc { + return func(ctx *Context, r *http.Request) http.Handler { + // Always checks the same name context + if ctx.Name != ctx.vars["name"] { + t.Fatalf("unexpected name: %q != %q", ctx.Name, "foo/bar") + } + + // Check that we have all that is expected + for expectedK, expectedV := range expectedVars { + if ctx.vars[expectedK] != expectedV { + t.Fatalf("unexpected %s in context vars: %q != %q", expectedK, ctx.vars[expectedK], expectedV) + } + } + + // Check that we only have variables that are expected + for k, v := range ctx.vars { + _, ok := expectedVars[k] + + if !ok { // name is checked on context + // We have an unexpected key, fail + t.Fatalf("unexpected key %q in vars with value %q", k, v) + } + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + } + } + + // unflatten a list of variables, suitable for gorilla/mux, to a map[string]string + unflatten := func(vars []string) map[string]string { + m := make(map[string]string) + for i := 0; i < len(vars)-1; i = i + 2 { + m[vars[i]] = vars[i+1] + } + + return m + } + + for _, testcase := range []struct { + endpoint string + vars []string + }{ + { + endpoint: routeNameImageManifest, + vars: []string{ + "name", "foo/bar", + "tag", "sometag", + }, + }, + { + endpoint: routeNameTags, + vars: []string{ + "name", "foo/bar", + }, + }, + { + endpoint: routeNameLayer, + vars: []string{ + "name", "foo/bar", + "tarsum", "thetarsum", + }, + }, + { + endpoint: routeNameLayerUpload, + vars: []string{ + "name", "foo/bar", + "tarsum", "thetarsum", + }, + }, + { + endpoint: routeNameLayerUploadResume, + vars: []string{ + "name", "foo/bar", + "tarsum", "thetarsum", + "uuid", "theuuid", + }, + }, + } { + app.register(testcase.endpoint, varCheckingDispatcher(unflatten(testcase.vars))) + route := router.GetRoute(testcase.endpoint).Host(serverURL.Host) + u, err := route.URL(testcase.vars...) + + if err != nil { + t.Fatal(err) + } + + resp, err := http.Get(u.String()) + + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: %v != %v", resp.StatusCode, http.StatusOK) + } + } +} diff --git a/context.go b/context.go new file mode 100644 index 00000000..a5706b4e --- /dev/null +++ b/context.go @@ -0,0 +1,34 @@ +package registry + +import ( + "github.com/Sirupsen/logrus" +) + +// Context should contain the request specific context for use in across +// handlers. Resources that don't need to be shared across handlers should not +// be on this object. +type Context struct { + // App points to the application structure that created this context. + *App + + // Name is the prefix for the current request. Corresponds to the + // namespace/repository associated with the image. + Name string + + // Errors is a collection of errors encountered during the request to be + // returned to the client API. If errors are added to the collection, the + // handler *must not* start the response via http.ResponseWriter. + 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 + // assignment. + vars map[string]string + + // log provides a context specific logger. + log *logrus.Entry +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 00000000..b3b9d744 --- /dev/null +++ b/helpers.go @@ -0,0 +1,20 @@ +package registry + +import ( + "encoding/json" + "net/http" +) + +// serveJSON marshals v and sets the content-type header to +// 'application/json'. If a different status code is required, call +// ResponseWriter.WriteHeader before this function. +func serveJSON(w http.ResponseWriter, v interface{}) error { + w.Header().Set("Content-Type", "application/json") + enc := json.NewEncoder(w) + + if err := enc.Encode(v); err != nil { + return err + } + + return nil +} diff --git a/images.go b/images.go new file mode 100644 index 00000000..f16a3560 --- /dev/null +++ b/images.go @@ -0,0 +1,46 @@ +package registry + +import ( + "net/http" + + "github.com/gorilla/handlers" +) + +// imageManifestDispatcher takes the request context and builds the +// appropriate handler for handling image manifest requests. +func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { + imageManifestHandler := &imageManifestHandler{ + Context: ctx, + Tag: ctx.vars["tag"], + } + + imageManifestHandler.log = imageManifestHandler.log.WithField("tag", imageManifestHandler.Tag) + + return handlers.MethodHandler{ + "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), + "PUT": http.HandlerFunc(imageManifestHandler.PutImageManifest), + "DELETE": http.HandlerFunc(imageManifestHandler.DeleteImageManifest), + } +} + +// imageManifestHandler handles http operations on image manifests. +type imageManifestHandler struct { + *Context + + Tag string +} + +// GetImageManifest fetches the image manifest from the storage backend, if it exists. +func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { + +} + +// PutImageManifest validates and stores and image in the registry. +func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { + +} + +// DeleteImageManifest removes the image with the given tag from the registry. +func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { + +} diff --git a/layer.go b/layer.go new file mode 100644 index 00000000..96920a8e --- /dev/null +++ b/layer.go @@ -0,0 +1,34 @@ +package registry + +import ( + "net/http" + + "github.com/gorilla/handlers" +) + +// layerDispatcher uses the request context to build a layerHandler. +func layerDispatcher(ctx *Context, r *http.Request) http.Handler { + layerHandler := &layerHandler{ + Context: ctx, + TarSum: ctx.vars["tarsum"], + } + + layerHandler.log = layerHandler.log.WithField("tarsum", layerHandler.TarSum) + + return handlers.MethodHandler{ + "GET": http.HandlerFunc(layerHandler.GetLayer), + } +} + +// layerHandler serves http layer requests. +type layerHandler struct { + *Context + + TarSum string +} + +// GetLayer fetches the binary data from backend storage returns it in the +// response. +func (lh *layerHandler) GetLayer(w http.ResponseWriter, r *http.Request) { + +} diff --git a/layerupload.go b/layerupload.go new file mode 100644 index 00000000..3eb2ff9a --- /dev/null +++ b/layerupload.go @@ -0,0 +1,63 @@ +package registry + +import ( + "net/http" + + "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 { + layerUploadHandler := &layerUploadHandler{ + Context: ctx, + TarSum: ctx.vars["tarsum"], + UUID: ctx.vars["uuid"], + } + + layerUploadHandler.log = layerUploadHandler.log.WithField("tarsum", layerUploadHandler.TarSum) + + if layerUploadHandler.UUID != "" { + layerUploadHandler.log = layerUploadHandler.log.WithField("uuid", layerUploadHandler.UUID) + } + + return handlers.MethodHandler{ + "POST": http.HandlerFunc(layerUploadHandler.StartLayerUpload), + "GET": http.HandlerFunc(layerUploadHandler.GetUploadStatus), + "PUT": http.HandlerFunc(layerUploadHandler.PutLayerChunk), + "DELETE": http.HandlerFunc(layerUploadHandler.CancelLayerUpload), + } +} + +// layerUploadHandler handles the http layer upload process. +type layerUploadHandler struct { + *Context + + // TarSum is the unique identifier of the layer being uploaded. + TarSum string + + // UUID identifies the upload instance for the current request. + UUID string +} + +// StartLayerUpload begins the layer upload process and allocates a server- +// side upload session. +func (luh *layerUploadHandler) StartLayerUpload(w http.ResponseWriter, r *http.Request) { + +} + +// GetUploadStatus returns the status of a given upload, identified by uuid. +func (luh *layerUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { + +} + +// PutLayerChunk receives a layer chunk during the layer upload process, +// possible completing the upload with a checksum and length. +func (luh *layerUploadHandler) PutLayerChunk(w http.ResponseWriter, r *http.Request) { + +} + +// CancelLayerUpload cancels an in-progress upload of a layer. +func (luh *layerUploadHandler) CancelLayerUpload(w http.ResponseWriter, r *http.Request) { + +} diff --git a/routes.go b/routes.go index 10c9e398..d4942696 100644 --- a/routes.go +++ b/routes.go @@ -5,21 +5,21 @@ import ( ) const ( - routeNameRoot = "root" - routeNameName = "name" - routeNameImageManifest = "image-manifest" - routeNameTags = "tags" - routeNameLayer = "layer" - routeNameStartLayerUpload = "start-layer-upload" - routeNameLayerUpload = "layer-upload" + routeNameRoot = "root" + routeNameName = "name" + routeNameImageManifest = "image-manifest" + routeNameTags = "tags" + routeNameLayer = "layer" + routeNameLayerUpload = "layer-upload" + routeNameLayerUploadResume = "layer-upload-resume" ) var allEndpoints = []string{ routeNameImageManifest, routeNameTags, routeNameLayer, - routeNameStartLayerUpload, routeNameLayerUpload, + routeNameLayerUploadResume, } // v2APIRouter builds a gorilla router with named routes for the various API @@ -59,14 +59,14 @@ func v2APIRouter() *mux.Router { // POST /v2//layer//upload/ Layer Upload Initiate an upload of the layer identified by tarsum. Requires length and a checksum parameter. namedRouter. Path("/layer/{tarsum}/upload/"). - Name(routeNameStartLayerUpload) + Name(routeNameLayerUpload) // GET /v2//layer//upload/ Layer Upload Get the status of the upload identified by tarsum and uuid. // PUT /v2//layer//upload/ Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. // DELETE /v2//layer//upload/ Layer Upload Cancel the upload identified by layer and uuid namedRouter. Path("/layer/{tarsum}/upload/{uuid}"). - Name(routeNameLayerUpload) + Name(routeNameLayerUploadResume) return router } diff --git a/routes_test.go b/routes_test.go index 6b1daf80..e3ef371a 100644 --- a/routes_test.go +++ b/routes_test.go @@ -76,7 +76,7 @@ func TestRouter(t *testing.T) { }, }, { - routeName: routeNameStartLayerUpload, + routeName: routeNameLayerUpload, expectedRouteInfo: routeInfo{ RequestURI: "/v2/foo/bar/layer/tarsum/upload/", Vars: map[string]string{ @@ -86,7 +86,7 @@ func TestRouter(t *testing.T) { }, }, { - routeName: routeNameLayerUpload, + routeName: routeNameLayerUploadResume, expectedRouteInfo: routeInfo{ RequestURI: "/v2/foo/bar/layer/tarsum/upload/uuid", Vars: map[string]string{ diff --git a/tags.go b/tags.go new file mode 100644 index 00000000..d8cea3d3 --- /dev/null +++ b/tags.go @@ -0,0 +1,28 @@ +package registry + +import ( + "net/http" + + "github.com/gorilla/handlers" +) + +// tagsDispatcher constructs the tags handler api endpoint. +func tagsDispatcher(ctx *Context, r *http.Request) http.Handler { + tagsHandler := &tagsHandler{ + Context: ctx, + } + + return handlers.MethodHandler{ + "GET": http.HandlerFunc(tagsHandler.GetTags), + } +} + +// tagsHandler handles requests for lists of tags under a repository name. +type tagsHandler struct { + *Context +} + +// GetTags returns a json list of tags for a specific image name. +func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { + // TODO(stevvooe): Implement this method. +} diff --git a/util.go b/util.go new file mode 100644 index 00000000..976ddf31 --- /dev/null +++ b/util.go @@ -0,0 +1,27 @@ +package registry + +import ( + "net/http" + "reflect" + "runtime" + + "github.com/gorilla/handlers" +) + +// functionName returns the name of the function fn. +func functionName(fn interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() +} + +// resolveHandlerName attempts to resolve a nice, pretty name for the passed +// in handler. +func resolveHandlerName(method string, handler http.Handler) string { + switch v := handler.(type) { + case handlers.MethodHandler: + return functionName(v[method]) + case http.HandlerFunc: + return functionName(v) + default: + return functionName(handler.ServeHTTP) + } +}