From 0618a2ebd73fe2965cc5ff3f67e69162bedd7b74 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 10 Nov 2014 18:26:06 -0800 Subject: [PATCH 1/2] Clearer names for layer upload routes --- routes.go | 20 ++++++++++---------- routes_test.go | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) 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{ From 22c9f45598491efe30718a3c5260b4fc170a4883 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Mon, 10 Nov 2014 18:57:38 -0800 Subject: [PATCH 2/2] Carve out initial application structure This changeset defines the application structure to be used for the http side of the new registry. The main components are the App and Context structs. The App context is instance global and manages global configuration and resources. Context contains request-specific resources that may be created as a by-product of an in-flight request. To latently construct per-request handlers and leverage gorilla/mux, a dispatch structure has been propped up next to the main handler flow. Without this, a router and all handlers need to be constructed on every request. By constructing handlers on each request, we ensure thread isolation and can carefully control the security context of in-flight requests. There are unit tests covering this functionality. --- app.go | 94 ++++++++++++++++++++++++++++++++++++ app_test.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++ context.go | 34 +++++++++++++ helpers.go | 20 ++++++++ images.go | 46 ++++++++++++++++++ layer.go | 34 +++++++++++++ layerupload.go | 63 ++++++++++++++++++++++++ tags.go | 28 +++++++++++ util.go | 27 +++++++++++ 9 files changed, 473 insertions(+) create mode 100644 app.go create mode 100644 app_test.go create mode 100644 context.go create mode 100644 helpers.go create mode 100644 images.go create mode 100644 layer.go create mode 100644 layerupload.go create mode 100644 tags.go create mode 100644 util.go 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/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) + } +}