allow fallthrough handling from go-restful routes

This commit is contained in:
deads2k 2017-03-10 08:10:53 -05:00
parent 0a6d82d8e7
commit 02efeeaf40
20 changed files with 123 additions and 89 deletions

View File

@ -223,7 +223,7 @@ func NonBlockingRun(s *options.ServerRunOptions, stopCh <-chan struct{}) error {
return err
}
routes.UIRedirect{}.Install(m.HandlerContainer)
routes.UIRedirect{}.Install(m.FallThroughHandler)
routes.Logs{}.Install(m.HandlerContainer)
installFederationAPIs(m, genericConfig.RESTOptionsGetter)

View File

@ -216,7 +216,7 @@ func (c completedConfig) New() (*Master, error) {
}
if c.EnableUISupport {
routes.UIRedirect{}.Install(s.HandlerContainer)
routes.UIRedirect{}.Install(s.FallThroughHandler)
}
if c.EnableLogsSupport {
routes.Logs{}.Install(s.HandlerContainer)

View File

@ -40,7 +40,7 @@ import (
// TestValidOpenAPISpec verifies that the open api is added
// at the proper endpoint and the spec is valid.
func TestValidOpenAPISpec(t *testing.T) {
_, etcdserver, config, assert := setUp(t)
etcdserver, config, assert := setUp(t)
defer etcdserver.Terminate(t)
config.GenericConfig.EnableIndex = true

View File

@ -60,7 +60,7 @@ import (
)
// setUp is a convience function for setting up for (most) tests.
func setUp(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *assert.Assertions) {
func setUp(t *testing.T) (*etcdtesting.EtcdTestServer, Config, *assert.Assertions) {
server, storageConfig := etcdtesting.NewUnsecuredEtcd3TestClientServer(t, api.Scheme)
config := &Config{
@ -101,16 +101,11 @@ func setUp(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *assert.
TLSClientConfig: &tls.Config{},
})
master, err := config.Complete().New()
if err != nil {
t.Fatal(err)
}
return master, server, *config, assert.New(t)
return server, *config, assert.New(t)
}
func newMaster(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *assert.Assertions) {
_, etcdserver, config, assert := setUp(t)
etcdserver, config, assert := setUp(t)
master, err := config.Complete().New()
if err != nil {
@ -136,7 +131,7 @@ func limitedAPIResourceConfigSource() *serverstorage.ResourceConfig {
// newLimitedMaster only enables the core group, the extensions group, the batch group, and the autoscaling group.
func newLimitedMaster(t *testing.T) (*Master, *etcdtesting.EtcdTestServer, Config, *assert.Assertions) {
_, etcdserver, config, assert := setUp(t)
etcdserver, config, assert := setUp(t)
config.APIResourceConfigSource = limitedAPIResourceConfigSource()
master, err := config.Complete().New()
if err != nil {

View File

@ -27,8 +27,8 @@ const dashboardPath = "/api/v1/namespaces/kube-system/services/kubernetes-dashbo
// UIRediect redirects /ui to the kube-ui proxy path.
type UIRedirect struct{}
func (r UIRedirect) Install(c *mux.APIContainer) {
c.NonSwaggerRoutes.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) {
func (r UIRedirect) Install(c *mux.PathRecorderMux) {
c.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, dashboardPath, http.StatusTemporaryRedirect)
})
}

View File

@ -107,6 +107,10 @@ type Config struct {
// Will default to a value based on secure serving info and available ipv4 IPs.
ExternalAddress string
// FallThroughHandler is the final HTTP handler in the chain. If it is nil, one will be created for you.
// It comes after all filters and the API handling
FallThroughHandler *mux.PathRecorderMux
//===========================================================================
// Fields you probably don't care about changing
//===========================================================================
@ -337,6 +341,9 @@ func (c *Config) Complete() completedConfig {
tokenAuthorizer := authorizerfactory.NewPrivilegedGroups(user.SystemPrivilegedGroup)
c.Authorizer = authorizerunion.New(tokenAuthorizer, c.Authorizer)
}
if c.FallThroughHandler == nil {
c.FallThroughHandler = mux.NewPathRecorderMux()
}
return completedConfig{c}
}
@ -392,6 +399,8 @@ func (c completedConfig) New() (*GenericAPIServer, error) {
apiGroupsForDiscovery: map[string]metav1.APIGroup{},
FallThroughHandler: c.FallThroughHandler,
swaggerConfig: c.SwaggerConfig,
openAPIConfig: c.OpenAPIConfig,
@ -399,7 +408,7 @@ func (c completedConfig) New() (*GenericAPIServer, error) {
healthzChecks: c.HealthzChecks,
}
s.HandlerContainer = mux.NewAPIContainer(http.NewServeMux(), c.Serializer)
s.HandlerContainer = mux.NewAPIContainer(http.NewServeMux(), c.Serializer, s.FallThroughHandler)
if s.openAPIConfig != nil {
if s.openAPIConfig.Info == nil {
@ -447,22 +456,22 @@ func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) (secure, insec
func (s *GenericAPIServer) installAPI(c *Config) {
if c.EnableIndex {
routes.Index{}.Install(s.HandlerContainer)
routes.Index{}.Install(s.HandlerContainer, s.FallThroughHandler)
}
if c.SwaggerConfig != nil && c.EnableSwaggerUI {
routes.SwaggerUI{}.Install(s.HandlerContainer)
routes.SwaggerUI{}.Install(s.FallThroughHandler)
}
if c.EnableProfiling {
routes.Profiling{}.Install(s.HandlerContainer)
routes.Profiling{}.Install(s.FallThroughHandler)
if c.EnableContentionProfiling {
goruntime.SetBlockProfileRate(1)
}
}
if c.EnableMetrics {
if c.EnableProfiling {
routes.MetricsWithReset{}.Install(s.HandlerContainer)
routes.MetricsWithReset{}.Install(s.FallThroughHandler)
} else {
routes.DefaultMetrics{}.Install(s.HandlerContainer)
routes.DefaultMetrics{}.Install(s.FallThroughHandler)
}
}
routes.Version{Version: c.Version}.Install(s.HandlerContainer)

View File

@ -44,6 +44,7 @@ import (
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/server/healthz"
"k8s.io/apiserver/pkg/server/mux"
genericmux "k8s.io/apiserver/pkg/server/mux"
"k8s.io/apiserver/pkg/server/routes"
restclient "k8s.io/client-go/rest"
@ -125,6 +126,9 @@ type GenericAPIServer struct {
// "Outputs"
Handler http.Handler
InsecureHandler http.Handler
// FallThroughHandler is the final HTTP handler in the chain.
// It comes after all filters and the API handling
FallThroughHandler *mux.PathRecorderMux
// Map storing information about all groups to be exposed in discovery response.
// The map is from name to the group.
@ -179,7 +183,7 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
if s.openAPIConfig != nil {
routes.OpenAPI{
Config: s.openAPIConfig,
}.Install(s.HandlerContainer)
}.Install(s.HandlerContainer, s.FallThroughHandler)
}
s.installHealthz()

View File

@ -48,6 +48,7 @@ import (
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/server/mux"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
restclient "k8s.io/client-go/rest"
)
@ -90,6 +91,7 @@ func setUp(t *testing.T) (*etcdtesting.EtcdTestServer, Config, *assert.Assertion
config.RequestContextMapper = genericapirequest.NewRequestContextMapper()
config.LegacyAPIGroupPrefixes = sets.NewString("/api")
config.LoopbackClientConfig = &restclient.Config{}
config.FallThroughHandler = mux.NewPathRecorderMux()
// TODO restore this test, but right now, eliminate our cycle
// config.OpenAPIConfig = DefaultOpenAPIConfig(testGetOpenAPIDefinitions, runtime.NewScheme())
@ -352,8 +354,8 @@ func TestCustomHandlerChain(t *testing.T) {
t.Fatalf("Error in bringing up the server: %v", err)
}
s.HandlerContainer.NonSwaggerRoutes.Handle("/nonswagger", handler)
s.HandlerContainer.UnlistedRoutes.Handle("/secret", handler)
s.FallThroughHandler.Handle("/nonswagger", handler)
s.FallThroughHandler.Handle("/secret", handler)
type Test struct {
handler http.Handler

View File

@ -41,5 +41,5 @@ func (s *GenericAPIServer) installHealthz() {
defer s.healthzLock.Unlock()
s.healthzCreated = true
healthz.InstallHandler(&s.HandlerContainer.NonSwaggerRoutes, s.healthzChecks...)
healthz.InstallHandler(s.FallThroughHandler, s.healthzChecks...)
}

View File

@ -35,22 +35,12 @@ import (
// handlers that do not show up in swagger or in /
type APIContainer struct {
*restful.Container
// NonSwaggerRoutes are recorded and are visible at /, but do not show up in Swagger.
NonSwaggerRoutes PathRecorderMux
// UnlistedRoutes are not recorded, therefore not visible at / and do not show up in Swagger.
UnlistedRoutes *http.ServeMux
}
// NewAPIContainer constructs a new container for APIs
func NewAPIContainer(mux *http.ServeMux, s runtime.NegotiatedSerializer) *APIContainer {
func NewAPIContainer(mux *http.ServeMux, s runtime.NegotiatedSerializer, defaultMux http.Handler) *APIContainer {
c := APIContainer{
Container: restful.NewContainer(),
NonSwaggerRoutes: PathRecorderMux{
mux: mux,
},
UnlistedRoutes: mux,
}
c.Container.ServeMux = mux
c.Container.Router(restful.CurlyRouter{}) // e.g. for proxy/{kind}/{name}/{*}
@ -61,6 +51,10 @@ func NewAPIContainer(mux *http.ServeMux, s runtime.NegotiatedSerializer) *APICon
serviceErrorHandler(s, serviceErr, request, response)
})
// register the defaultHandler for everything. This will allow an unhandled request to fall through to another handler instead of
// ending up with a forced 404
c.Container.Handle("/", defaultMux)
return &c
}

View File

@ -17,47 +17,83 @@ limitations under the License.
package mux
import (
"fmt"
"net/http"
"runtime/debug"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
// Mux is an object that can register http handlers.
type Mux interface {
Handle(pattern string, handler http.Handler)
HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}
// PathRecorderMux wraps a mux object and records the registered paths. It is _not_ go routine safe.
// PathRecorderMux wraps a mux object and records the registered exposedPaths. It is _not_ go routine safe.
type PathRecorderMux struct {
mux Mux
paths []string
mux *http.ServeMux
exposedPaths []string
// pathStacks holds the stacks of all registered paths. This allows us to show a more helpful message
// before the "http: multiple registrations for %s" panic.
pathStacks map[string]string
}
// NewPathRecorderMux creates a new PathRecorderMux with the given mux as the base mux.
func NewPathRecorderMux(mux Mux) *PathRecorderMux {
func NewPathRecorderMux() *PathRecorderMux {
return &PathRecorderMux{
mux: mux,
mux: http.NewServeMux(),
pathStacks: map[string]string{},
}
}
// BaseMux returns the underlying mux.
func (m *PathRecorderMux) BaseMux() Mux {
return m.mux
}
// HandledPaths returns the registered handler paths.
// HandledPaths returns the registered handler exposedPaths.
func (m *PathRecorderMux) HandledPaths() []string {
return append([]string{}, m.paths...)
return append([]string{}, m.exposedPaths...)
}
// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (m *PathRecorderMux) Handle(path string, handler http.Handler) {
m.paths = append(m.paths, path)
if existingStack, ok := m.pathStacks[path]; ok {
utilruntime.HandleError(fmt.Errorf("registered %q from %v", path, existingStack))
}
m.pathStacks[path] = string(debug.Stack())
m.exposedPaths = append(m.exposedPaths, path)
m.mux.Handle(path, handler)
}
// HandleFunc registers the handler function for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (m *PathRecorderMux) HandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) {
m.paths = append(m.paths, path)
if existingStack, ok := m.pathStacks[path]; ok {
utilruntime.HandleError(fmt.Errorf("registered %q from %v", path, existingStack))
}
m.pathStacks[path] = string(debug.Stack())
m.exposedPaths = append(m.exposedPaths, path)
m.mux.HandleFunc(path, handler)
}
// UnlistedHandle registers the handler for the given pattern, but doesn't list it.
// If a handler already exists for pattern, Handle panics.
func (m *PathRecorderMux) UnlistedHandle(path string, handler http.Handler) {
if existingStack, ok := m.pathStacks[path]; ok {
utilruntime.HandleError(fmt.Errorf("registered %q from %v", path, existingStack))
}
m.pathStacks[path] = string(debug.Stack())
m.mux.Handle(path, handler)
}
// UnlistedHandleFunc registers the handler function for the given pattern, but doesn't list it.
// If a handler already exists for pattern, Handle panics.
func (m *PathRecorderMux) UnlistedHandleFunc(path string, handler func(http.ResponseWriter, *http.Request)) {
if existingStack, ok := m.pathStacks[path]; ok {
utilruntime.HandleError(fmt.Errorf("registered %q from %v", path, existingStack))
}
m.pathStacks[path] = string(debug.Stack())
m.mux.HandleFunc(path, handler)
}
// ServeHTTP makes it an http.Handler
func (m *PathRecorderMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.mux.ServeHTTP(w, r)
}

View File

@ -23,18 +23,10 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewAPIContainer(t *testing.T) {
mux := http.NewServeMux()
c := NewAPIContainer(mux, nil)
assert.Equal(t, mux, c.UnlistedRoutes, "UnlistedRoutes ServeMux's do not match")
assert.Equal(t, mux, c.Container.ServeMux, "Container ServeMux's do not match")
}
func TestSecretHandlers(t *testing.T) {
mux := http.NewServeMux()
c := NewAPIContainer(mux, nil)
c.UnlistedRoutes.HandleFunc("/secret", func(http.ResponseWriter, *http.Request) {})
c.NonSwaggerRoutes.HandleFunc("/nonswagger", func(http.ResponseWriter, *http.Request) {})
assert.NotContains(t, c.NonSwaggerRoutes.HandledPaths(), "/secret")
assert.Contains(t, c.NonSwaggerRoutes.HandledPaths(), "/nonswagger")
c := NewPathRecorderMux()
c.UnlistedHandleFunc("/secret", func(http.ResponseWriter, *http.Request) {})
c.HandleFunc("/nonswagger", func(http.ResponseWriter, *http.Request) {})
assert.NotContains(t, c.HandledPaths(), "/secret")
assert.Contains(t, c.HandledPaths(), "/nonswagger")
}

View File

@ -44,7 +44,7 @@ type openAPI struct {
}
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification.
func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *openapi.Config, container *genericmux.APIContainer) (err error) {
func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *openapi.Config, mux *genericmux.PathRecorderMux) (err error) {
o := openAPI{
config: config,
servePath: servePath,
@ -63,7 +63,7 @@ func RegisterOpenAPIService(servePath string, webServices []*restful.WebService,
return err
}
container.UnlistedRoutes.HandleFunc(servePath, func(w http.ResponseWriter, r *http.Request) {
mux.UnlistedHandleFunc(servePath, func(w http.ResponseWriter, r *http.Request) {
resp := restful.NewResponse(w)
if r.URL.Path != servePath {
resp.WriteErrorString(http.StatusNotFound, "Path not found!")

View File

@ -29,8 +29,8 @@ import (
type Index struct{}
// Install adds the Index webservice to the given mux.
func (i Index) Install(c *mux.APIContainer) {
c.UnlistedRoutes.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
func (i Index) Install(c *mux.APIContainer, mux *mux.PathRecorderMux) {
mux.UnlistedHandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
// Since "/" matches all paths, handleIndex is called for all paths for which there is no handler api.Registry.
@ -43,7 +43,7 @@ func (i Index) Install(c *mux.APIContainer) {
handledPaths = append(handledPaths, ws.RootPath())
}
// Extract the paths handled using mux handler.
handledPaths = append(handledPaths, c.NonSwaggerRoutes.HandledPaths()...)
handledPaths = append(handledPaths, mux.HandledPaths()...)
sort.Strings(handledPaths)
responsewriters.WriteRawJSON(status, metav1.RootPaths{Paths: handledPaths}, w)
})

View File

@ -31,8 +31,8 @@ import (
type DefaultMetrics struct{}
// Install adds the DefaultMetrics handler
func (m DefaultMetrics) Install(c *mux.APIContainer) {
c.NonSwaggerRoutes.Handle("/metrics", prometheus.Handler())
func (m DefaultMetrics) Install(c *mux.PathRecorderMux) {
c.Handle("/metrics", prometheus.Handler())
}
// MetricsWithReset install the prometheus metrics handler extended with support for the DELETE method
@ -40,9 +40,9 @@ func (m DefaultMetrics) Install(c *mux.APIContainer) {
type MetricsWithReset struct{}
// Install adds the MetricsWithReset handler
func (m MetricsWithReset) Install(c *mux.APIContainer) {
func (m MetricsWithReset) Install(c *mux.PathRecorderMux) {
defaultMetricsHandler := prometheus.Handler().ServeHTTP
c.NonSwaggerRoutes.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) {
c.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) {
if req.Method == "DELETE" {
apimetrics.Reset()
etcdmetrics.Reset()

View File

@ -30,8 +30,8 @@ type OpenAPI struct {
}
// Install adds the SwaggerUI webservice to the given mux.
func (oa OpenAPI) Install(c *mux.APIContainer) {
err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, c)
func (oa OpenAPI) Install(c *mux.APIContainer, mux *mux.PathRecorderMux) {
err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux)
if err != nil {
glog.Fatalf("Failed to register open api spec for root: %v", err)
}

View File

@ -26,9 +26,9 @@ import (
type Profiling struct{}
// Install adds the Profiling webservice to the given mux.
func (d Profiling) Install(c *mux.APIContainer) {
c.UnlistedRoutes.HandleFunc("/debug/pprof/", pprof.Index)
c.UnlistedRoutes.HandleFunc("/debug/pprof/profile", pprof.Profile)
c.UnlistedRoutes.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
c.UnlistedRoutes.HandleFunc("/debug/pprof/trace", pprof.Trace)
func (d Profiling) Install(c *mux.PathRecorderMux) {
c.UnlistedHandleFunc("/debug/pprof/", pprof.Index)
c.UnlistedHandleFunc("/debug/pprof/profile", pprof.Profile)
c.UnlistedHandleFunc("/debug/pprof/symbol", pprof.Symbol)
c.UnlistedHandleFunc("/debug/pprof/trace", pprof.Trace)
}

View File

@ -29,12 +29,12 @@ import (
type SwaggerUI struct{}
// Install adds the SwaggerUI webservice to the given mux.
func (l SwaggerUI) Install(c *mux.APIContainer) {
func (l SwaggerUI) Install(c *mux.PathRecorderMux) {
fileServer := http.FileServer(&assetfs.AssetFS{
Asset: swagger.Asset,
AssetDir: swagger.AssetDir,
Prefix: "third_party/swagger-ui",
})
prefix := "/swagger-ui/"
c.NonSwaggerRoutes.Handle(prefix, http.StripPrefix(prefix, fileServer))
c.Handle(prefix, http.StripPrefix(prefix, fileServer))
}

View File

@ -250,8 +250,8 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) {
endpointsLister: s.endpointsLister,
}
// aggregation is protected
s.GenericAPIServer.HandlerContainer.UnlistedRoutes.Handle(groupPath, groupDiscoveryHandler)
s.GenericAPIServer.HandlerContainer.UnlistedRoutes.Handle(groupPath+"/", groupDiscoveryHandler)
s.GenericAPIServer.FallThroughHandler.UnlistedHandle(groupPath, groupDiscoveryHandler)
s.GenericAPIServer.FallThroughHandler.UnlistedHandle(groupPath+"/", groupDiscoveryHandler)
s.handledGroups.Insert(apiService.Spec.Group)
}

4
vendor/BUILD vendored
View File

@ -10488,6 +10488,7 @@ go_test(
"//vendor:k8s.io/apiserver/pkg/authorization/authorizer",
"//vendor:k8s.io/apiserver/pkg/endpoints/request",
"//vendor:k8s.io/apiserver/pkg/registry/rest",
"//vendor:k8s.io/apiserver/pkg/server/mux",
"//vendor:k8s.io/apiserver/pkg/storage/etcd/testing",
"//vendor:k8s.io/client-go/rest",
],
@ -10627,7 +10628,7 @@ go_library(
go_test(
name = "k8s.io/apiserver/pkg/server/mux_test",
srcs = ["k8s.io/apiserver/pkg/server/mux/container_test.go"],
srcs = ["k8s.io/apiserver/pkg/server/mux/pathrecorder_test.go"],
library = ":k8s.io/apiserver/pkg/server/mux",
tags = ["automanaged"],
deps = ["//vendor:github.com/stretchr/testify/assert"],
@ -10647,6 +10648,7 @@ go_library(
"//vendor:k8s.io/apimachinery/pkg/api/errors",
"//vendor:k8s.io/apimachinery/pkg/runtime",
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
"//vendor:k8s.io/apiserver/pkg/endpoints/handlers/responsewriters",
],
)