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.
This commit is contained in:
Stephen J Day 2014-11-10 18:57:38 -08:00
parent 0618a2ebd7
commit 22c9f45598
9 changed files with 473 additions and 0 deletions

94
app.go Normal file
View file

@ -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)
}
})
}

127
app_test.go Normal file
View file

@ -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)
}
}
}

34
context.go Normal file
View file

@ -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
}

20
helpers.go Normal file
View file

@ -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
}

46
images.go Normal file
View file

@ -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) {
}

34
layer.go Normal file
View file

@ -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) {
}

63
layerupload.go Normal file
View file

@ -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) {
}

28
tags.go Normal file
View file

@ -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.
}

27
util.go Normal file
View file

@ -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)
}
}