Merge pull request #712 from stevvooe/application-structure
Carve out initial application structure
This commit is contained in:
commit
d245a502b2
11 changed files with 485 additions and 12 deletions
94
app.go
Normal file
94
app.go
Normal 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
127
app_test.go
Normal 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
34
context.go
Normal 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
20
helpers.go
Normal 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
46
images.go
Normal 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
34
layer.go
Normal 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
63
layerupload.go
Normal 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) {
|
||||
|
||||
}
|
20
routes.go
20
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/<name>/layer/<tarsum>/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/<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.
|
||||
// DELETE /v2/<name>/layer/<tarsum>/upload/<uuid> Layer Upload Cancel the upload identified by layer and uuid
|
||||
namedRouter.
|
||||
Path("/layer/{tarsum}/upload/{uuid}").
|
||||
Name(routeNameLayerUpload)
|
||||
Name(routeNameLayerUploadResume)
|
||||
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
28
tags.go
Normal file
28
tags.go
Normal 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
27
util.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue