Merge pull request #712 from stevvooe/application-structure

Carve out initial application structure
This commit is contained in:
Olivier Gambier 2014-11-11 08:23:57 -08:00
commit d245a502b2
11 changed files with 485 additions and 12 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) {
}

View file

@ -10,16 +10,16 @@ const (
routeNameImageManifest = "image-manifest" routeNameImageManifest = "image-manifest"
routeNameTags = "tags" routeNameTags = "tags"
routeNameLayer = "layer" routeNameLayer = "layer"
routeNameStartLayerUpload = "start-layer-upload"
routeNameLayerUpload = "layer-upload" routeNameLayerUpload = "layer-upload"
routeNameLayerUploadResume = "layer-upload-resume"
) )
var allEndpoints = []string{ var allEndpoints = []string{
routeNameImageManifest, routeNameImageManifest,
routeNameTags, routeNameTags,
routeNameLayer, routeNameLayer,
routeNameStartLayerUpload,
routeNameLayerUpload, routeNameLayerUpload,
routeNameLayerUploadResume,
} }
// v2APIRouter builds a gorilla router with named routes for the various API // v2APIRouter builds a gorilla router with named routes for the various API
@ -59,14 +59,14 @@ func v2APIRouter() *mux.Router {
// POST /v2/<name>/layer/<tarsum>/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. Requires length and a checksum parameter. // POST /v2/<name>/layer/<tarsum>/upload/ Layer Upload Initiate an upload of the layer identified by tarsum. Requires length and a checksum parameter.
namedRouter. namedRouter.
Path("/layer/{tarsum}/upload/"). Path("/layer/{tarsum}/upload/").
Name(routeNameStartLayerUpload) Name(routeNameLayerUpload)
// GET /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid. // GET /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Get the status of the upload identified by tarsum and uuid.
// PUT /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid. // PUT /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Upload all or a chunk of the upload identified by tarsum and uuid.
// DELETE /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid // DELETE /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid
namedRouter. namedRouter.
Path("/layer/{tarsum}/upload/{uuid}"). Path("/layer/{tarsum}/upload/{uuid}").
Name(routeNameLayerUpload) Name(routeNameLayerUploadResume)
return router return router
} }

View file

@ -76,7 +76,7 @@ func TestRouter(t *testing.T) {
}, },
}, },
{ {
routeName: routeNameStartLayerUpload, routeName: routeNameLayerUpload,
expectedRouteInfo: routeInfo{ expectedRouteInfo: routeInfo{
RequestURI: "/v2/foo/bar/layer/tarsum/upload/", RequestURI: "/v2/foo/bar/layer/tarsum/upload/",
Vars: map[string]string{ Vars: map[string]string{
@ -86,7 +86,7 @@ func TestRouter(t *testing.T) {
}, },
}, },
{ {
routeName: routeNameLayerUpload, routeName: routeNameLayerUploadResume,
expectedRouteInfo: routeInfo{ expectedRouteInfo: routeInfo{
RequestURI: "/v2/foo/bar/layer/tarsum/upload/uuid", RequestURI: "/v2/foo/bar/layer/tarsum/upload/uuid",
Vars: map[string]string{ Vars: map[string]string{

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