forked from TrueCloudLab/distribution
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:
parent
0618a2ebd7
commit
22c9f45598
9 changed files with 473 additions and 0 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) {
|
||||||
|
|
||||||
|
}
|
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