Merge pull request #42886 from deads2k/server-02-fallthrough

Automatic merge from submit-queue

allow fallthrough handling from go-restful routes

This sets up the gorestful routes to fall through to a default handler and reorders the API to be ahead of the other endpoints.  This makes it possible to cleanly support cases of "match, fail, try this other handler" which we'll need for API server composition.

@kubernetes/sig-api-machinery-pr-reviews @ncdc
This commit is contained in:
Kubernetes Submit Queue
2017-03-25 15:56:05 -07:00
committed by GitHub
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

@@ -43,6 +43,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"
@@ -124,6 +125,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.
@@ -180,7 +184,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

@@ -287,8 +287,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

@@ -10501,6 +10501,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",
],
@@ -10640,7 +10641,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"],
@@ -10660,6 +10661,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",
],
)