diff --git a/configuration/configuration.go b/configuration/configuration.go index 9b5672d5d..8ba401c66 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -38,6 +38,8 @@ type Configuration struct { // Addr specifies the bind address for the registry instance. Addr string `yaml:"addr,omitempty"` + Prefix string `yaml:"prefix,omitempty"` + // Secret specifies the secret key which HMAC tokens are created with. Secret string `yaml:"secret,omitempty"` diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go index 2c6fafd02..e2007a2e3 100644 --- a/registry/api/v2/descriptors.go +++ b/registry/api/v2/descriptors.go @@ -1410,13 +1410,18 @@ var errorDescriptors = []ErrorDescriptor{ var errorCodeToDescriptors map[ErrorCode]ErrorDescriptor var idToDescriptors map[string]ErrorDescriptor +var routeDescriptorsMap map[string]RouteDescriptor func init() { errorCodeToDescriptors = make(map[ErrorCode]ErrorDescriptor, len(errorDescriptors)) idToDescriptors = make(map[string]ErrorDescriptor, len(errorDescriptors)) + routeDescriptorsMap = make(map[string]RouteDescriptor, len(routeDescriptors)) for _, descriptor := range errorDescriptors { errorCodeToDescriptors[descriptor.Code] = descriptor idToDescriptors[descriptor.Value] = descriptor } + for _, descriptor := range routeDescriptors { + routeDescriptorsMap[descriptor.Name] = descriptor + } } diff --git a/registry/api/v2/routes.go b/registry/api/v2/routes.go index ef9336009..69f9d9012 100644 --- a/registry/api/v2/routes.go +++ b/registry/api/v2/routes.go @@ -25,12 +25,23 @@ var allEndpoints = []string{ // methods. This can be used directly by both server implementations and // clients. func Router() *mux.Router { - router := mux.NewRouter(). - StrictSlash(true) + return RouterWithPrefix("") +} + +// 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 { router.Path(descriptor.Path).Name(descriptor.Name) } - return router + return rootRouter } diff --git a/registry/api/v2/routes_test.go b/registry/api/v2/routes_test.go index af4246162..dfd11082f 100644 --- a/registry/api/v2/routes_test.go +++ b/registry/api/v2/routes_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "github.com/gorilla/mux" @@ -24,8 +25,16 @@ type routeTestCase struct { // // This may go away as the application structure comes together. 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) { testCase := routeTestCase{ @@ -147,6 +156,8 @@ func TestRouter(t *testing.T) { StatusCode: http.StatusNotFound, }, } { + testcase.RequestURI = strings.TrimSuffix(prefix, "/") + testcase.RequestURI + // Register the endpoint route := router.GetRoute(testcase.RouteName) if route == nil { diff --git a/registry/api/v2/urls.go b/registry/api/v2/urls.go index 6f2fd6e8e..e36afdabf 100644 --- a/registry/api/v2/urls.go +++ b/registry/api/v2/urls.go @@ -3,6 +3,7 @@ package v2 import ( "net/http" "net/url" + "strings" "github.com/docker/distribution/digest" "github.com/gorilla/mux" @@ -64,11 +65,21 @@ func NewURLBuilderFromRequest(r *http.Request) *URLBuilder { host = forwardedHost } + basePath := routeDescriptorsMap[RouteNameBase].Path + + requestPath := r.URL.Path + index := strings.Index(requestPath, basePath) + u := &url.URL{ Scheme: scheme, 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) } @@ -171,6 +182,10 @@ func (cr clonedRoute) URL(pairs ...string) (*url.URL, error) { return nil, err } + if routeURL.Scheme == "" && routeURL.User == nil && routeURL.Host == "" { + routeURL.Path = routeURL.Path[1:] + } + return cr.root.ResolveReference(routeURL), nil } diff --git a/registry/api/v2/urls_test.go b/registry/api/v2/urls_test.go index d8001c2a4..237d0f615 100644 --- a/registry/api/v2/urls_test.go +++ b/registry/api/v2/urls_test.go @@ -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 { request *http.Request 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) + } + } + } +} diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go index a14e93dc9..f400f83e8 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -12,6 +12,7 @@ import ( "net/url" "os" "reflect" + "strings" "testing" "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. func TestLayerAPI(t *testing.T) { // 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 { - ctx := context.Background() config := configuration.Configuration{ Storage: configuration.Storage{ "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)) - builder, err := v2.NewURLBuilderFromString(server.URL) + builder, err := v2.NewURLBuilderFromString(server.URL + config.HTTP.Prefix) if err != nil { t.Fatalf("error creating url builder: %v", err) @@ -379,7 +419,7 @@ func newTestEnv(t *testing.T) *testEnv { return &testEnv{ pk: pk, ctx: ctx, - config: config, + config: *config, app: app, server: server, builder: builder, diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 2202de4af..199ca180f 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -64,7 +64,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App Config: configuration, Context: ctx, InstanceID: uuid.New(), - router: v2.Router(), + router: v2.RouterWithPrefix(configuration.HTTP.Prefix), } app.Context = ctxu.WithLogger(app.Context, ctxu.GetLogger(app, "app.id"))