Path prefix support for running registry somewhere other than root of server
Signed-off-by: David Lawrence <david.lawrence@docker.com> (github: endophage)master
							parent
							
								
									47a8ad7a61
								
							
						
					
					
						commit
						1700f518cb
					
				|  | @ -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"` | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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")) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue