Path prefix support for running registry somewhere other than root of server

Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)
This commit is contained in:
David Lawrence 2015-02-24 14:59:01 -08:00
parent 47a8ad7a61
commit 1700f518cb
8 changed files with 163 additions and 9 deletions

View file

@ -38,6 +38,8 @@ type Configuration struct {
// Addr specifies the bind address for the registry instance. // Addr specifies the bind address for the registry instance.
Addr string `yaml:"addr,omitempty"` Addr string `yaml:"addr,omitempty"`
Prefix string `yaml:"prefix,omitempty"`
// Secret specifies the secret key which HMAC tokens are created with. // Secret specifies the secret key which HMAC tokens are created with.
Secret string `yaml:"secret,omitempty"` Secret string `yaml:"secret,omitempty"`

View file

@ -1410,13 +1410,18 @@ var errorDescriptors = []ErrorDescriptor{
var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor
var idToDescriptors map[string]ErrorDescriptor var idToDescriptors map[string]ErrorDescriptor
var routeDescriptorsMap map[string]RouteDescriptor
func init() { func init() {
errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(errorDescriptors)) errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(errorDescriptors))
idToDescriptors = make(map[string]ErrorDescriptor, len(errorDescriptors)) idToDescriptors = make(map[string]ErrorDescriptor, len(errorDescriptors))
routeDescriptorsMap = make(map[string]RouteDescriptor, len(routeDescriptors))
for _, descriptor := range errorDescriptors { for _, descriptor := range errorDescriptors {
errorCodeToDescriptors[descriptor.Code] = descriptor errorCodeToDescriptors[descriptor.Code] = descriptor
idToDescriptors[descriptor.Value] = descriptor idToDescriptors[descriptor.Value] = descriptor
} }
for _, descriptor := range routeDescriptors {
routeDescriptorsMap[descriptor.Name] = descriptor
}
} }

View file

@ -25,12 +25,23 @@ var allEndpoints = []string{
// methods. This can be used directly by both server implementations and // methods. This can be used directly by both server implementations and
// clients. // clients.
func Router() *mux.Router { func Router() *mux.Router {
router := mux.NewRouter(). return RouterWithPrefix("")
StrictSlash(true) }
// RouterWithPrefix builds a gorilla router with a configured prefix
// on all routes.
func RouterWithPrefix(prefix string) *mux.Router {
rootRouter := mux.NewRouter()
router := rootRouter
if prefix != "" {
router = router.PathPrefix(prefix).Subrouter()
}
router.StrictSlash(true)
for _, descriptor := range routeDescriptors { for _, descriptor := range routeDescriptors {
router.Path(descriptor.Path).Name(descriptor.Name) router.Path(descriptor.Path).Name(descriptor.Name)
} }
return router return rootRouter
} }

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -24,8 +25,16 @@ type routeTestCase struct {
// //
// This may go away as the application structure comes together. // This may go away as the application structure comes together.
func TestRouter(t *testing.T) { func TestRouter(t *testing.T) {
baseTestRouter(t, "")
}
router := Router() func TestRouterWithPrefix(t *testing.T) {
baseTestRouter(t, "/prefix/")
}
func baseTestRouter(t *testing.T, prefix string) {
router := RouterWithPrefix(prefix)
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
testCase := routeTestCase{ testCase := routeTestCase{
@ -147,6 +156,8 @@ func TestRouter(t *testing.T) {
StatusCode: http.StatusNotFound, StatusCode: http.StatusNotFound,
}, },
} { } {
testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI
// Register the endpoint // Register the endpoint
route := router.GetRoute(testcase.RouteName) route := router.GetRoute(testcase.RouteName)
if route == nil { if route == nil {

View file

@ -3,6 +3,7 @@ package v2
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -64,11 +65,21 @@ func NewURLBuilderFromRequest(r *http.Request) *URLBuilder {
host = forwardedHost host = forwardedHost
} }
basePath := routeDescriptorsMap[RouteNameBase].Path
requestPath := r.URL.Path
index := strings.Index(requestPath, basePath)
u := &url.URL{ u := &url.URL{
Scheme: scheme, Scheme: scheme,
Host: host, Host: host,
} }
if index > 0 {
// N.B. index+1 is important because we want to include the trailing /
u.Path = requestPath[0 : index+1]
}
return NewURLBuilder(u) return NewURLBuilder(u)
} }
@ -171,6 +182,10 @@ func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) {
return nil, err return nil, err
} }
if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" {
routeURL.Path = routeURL.Path[1:]
}
return cr.root.ResolveReference(routeURL), nil return cr.root.ResolveReference(routeURL), nil
} }

View file

@ -108,6 +108,35 @@ func TestURLBuilder(t *testing.T) {
} }
} }
func TestURLBuilderWithPrefix(t *testing.T) {
roots := []string{
"http://example.com/prefix/",
"https://example.com/prefix/",
"http://localhost:5000/prefix/",
"https://localhost:5443/prefix/",
}
for _, root := range roots {
urlBuilder, err := NewURLBuilderFromString(root)
if err != nil {
t.Fatalf("unexpected error creating urlbuilder: %v", err)
}
for _, testCase := range makeURLBuilderTestCases(urlBuilder) {
url, err := testCase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testCase.description, err)
}
expectedURL := root[0:len(root)-1] + testCase.expectedPath
if url != expectedURL {
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
}
}
}
}
type builderFromRequestTestCase struct { type builderFromRequestTestCase struct {
request *http.Request request *http.Request
base string base string
@ -153,3 +182,44 @@ func TestBuilderFromRequest(t *testing.T) {
} }
} }
} }
func TestBuilderFromRequestWithPrefix(t *testing.T) {
u, err := url.Parse("http://example.com/prefix/v2/")
if err != nil {
t.Fatal(err)
}
forwardedProtoHeader := make(http.Header, 1)
forwardedProtoHeader.Set("X-Forwarded-Proto", "https")
testRequests := []struct {
request *http.Request
base string
}{
{
request: &http.Request{URL: u, Host: u.Host},
base: "http://example.com/prefix/",
},
{
request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader},
base: "https://example.com/prefix/",
},
}
for _, tr := range testRequests {
builder := NewURLBuilderFromRequest(tr.request)
for _, testCase := range makeURLBuilderTestCases(builder) {
url, err := testCase.build()
if err != nil {
t.Fatalf("%s: error building url: %v", testCase.description, err)
}
expectedURL := tr.base[0:len(tr.base)-1] + testCase.expectedPath
if url != expectedURL {
t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL)
}
}
}
}

View file

@ -12,6 +12,7 @@ import (
"net/url" "net/url"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
@ -57,6 +58,40 @@ func TestCheckAPI(t *testing.T) {
} }
} }
func TestURLPrefix(t *testing.T) {
config := configuration.Configuration{
Storage: configuration.Storage{
"inmemory": configuration.Parameters{},
},
}
config.HTTP.Prefix = "/test/"
env := newTestEnvWithConfig(t, &config)
baseURL, err := env.builder.BuildBaseURL()
if err != nil {
t.Fatalf("unexpected error building base url: %v", err)
}
parsed, _ := url.Parse(baseURL)
if !strings.HasPrefix(parsed.Path, config.HTTP.Prefix) {
t.Fatalf("Prefix %v not included in test url %v", config.HTTP.Prefix, baseURL)
}
resp, err := http.Get(baseURL)
if err != nil {
t.Fatalf("unexpected error issuing request: %v", err)
}
defer resp.Body.Close()
checkResponse(t, "issuing api base check", resp, http.StatusOK)
checkHeaders(t, resp, http.Header{
"Content-Type": []string{"application/json; charset=utf-8"},
"Content-Length": []string{"2"},
})
}
// TestLayerAPI conducts a full of the of the layer api. // TestLayerAPI conducts a full of the of the layer api.
func TestLayerAPI(t *testing.T) { func TestLayerAPI(t *testing.T) {
// TODO(stevvooe): This test code is complete junk but it should cover the // TODO(stevvooe): This test code is complete junk but it should cover the
@ -356,16 +391,21 @@ type testEnv struct {
} }
func newTestEnv(t *testing.T) *testEnv { func newTestEnv(t *testing.T) *testEnv {
ctx := context.Background()
config := configuration.Configuration{ config := configuration.Configuration{
Storage: configuration.Storage{ Storage: configuration.Storage{
"inmemory": configuration.Parameters{}, "inmemory": configuration.Parameters{},
}, },
} }
app := NewApp(ctx, config) return newTestEnvWithConfig(t, &config)
}
func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv {
ctx := context.Background()
app := NewApp(ctx, *config)
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
builder, err := v2.NewURLBuilderFromString(server.URL) builder, err := v2.NewURLBuilderFromString(server.URL + config.HTTP.Prefix)
if err != nil { if err != nil {
t.Fatalf("error creating url builder: %v", err) t.Fatalf("error creating url builder: %v", err)
@ -379,7 +419,7 @@ func newTestEnv(t *testing.T) *testEnv {
return &testEnv{ return &testEnv{
pk: pk, pk: pk,
ctx: ctx, ctx: ctx,
config: config, config: *config,
app: app, app: app,
server: server, server: server,
builder: builder, builder: builder,

View file

@ -64,7 +64,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
Config: configuration, Config: configuration,
Context: ctx, Context: ctx,
InstanceID: uuid.New(), InstanceID: uuid.New(),
router: v2.Router(), router: v2.RouterWithPrefix(configuration.HTTP.Prefix),
} }
app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "app.id")) app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "app.id"))