From 68cee1d9ace67d002d1b19b875de3907c42eb4be Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Wed, 28 Sep 2016 11:26:50 +0200 Subject: [PATCH] Make genericapiserver handler chain customizable --- .../cmd/federation-apiserver/app/server.go | 4 +- hack/.linted_packages | 1 + pkg/genericapiserver/config.go | 79 ++++++++-------- pkg/genericapiserver/genericapiserver.go | 78 ++++++---------- pkg/genericapiserver/genericapiserver_test.go | 93 ++++++++++++------- pkg/genericapiserver/mux/container.go | 52 +++++++++++ pkg/genericapiserver/mux/container_test.go | 40 ++++++++ pkg/genericapiserver/mux/doc.go | 18 ++++ .../mux/pathrecorder.go} | 4 +- pkg/genericapiserver/routes/index.go | 10 +- pkg/genericapiserver/routes/profiling.go | 13 ++- pkg/genericapiserver/routes/swaggerui.go | 7 +- pkg/genericapiserver/routes/version.go | 3 +- pkg/master/master.go | 14 +-- pkg/routes/logs.go | 4 +- pkg/routes/metrics.go | 11 +-- pkg/routes/ui.go | 8 +- test/integration/openshift/openshift_test.go | 1 - 18 files changed, 269 insertions(+), 171 deletions(-) create mode 100644 pkg/genericapiserver/mux/container.go create mode 100644 pkg/genericapiserver/mux/container_test.go create mode 100644 pkg/genericapiserver/mux/doc.go rename pkg/{apiserver/mux.go => genericapiserver/mux/pathrecorder.go} (96%) diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index e759a8f576c..01001285787 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -231,8 +231,8 @@ func Run(s *options.ServerRunOptions) error { return err } - routes.UIRedirect{}.Install(m.Mux, m.HandlerContainer) - routes.Logs{}.Install(m.Mux, m.HandlerContainer) + routes.UIRedirect{}.Install(m.HandlerContainer) + routes.Logs{}.Install(m.HandlerContainer) restOptionsFactory := restOptionsFactory{ storageFactory: storageFactory, diff --git a/hack/.linted_packages b/hack/.linted_packages index c00257c0de9..19d5c94df25 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -100,6 +100,7 @@ pkg/controller/volume/statusupdater pkg/conversion/queryparams pkg/credentialprovider/aws pkg/genericapiserver/filters +pkg/genericapiserver/mux pkg/genericapiserver/routes pkg/hyperkube pkg/kubelet/api diff --git a/pkg/genericapiserver/config.go b/pkg/genericapiserver/config.go index e14320acbb8..bd8c523debc 100644 --- a/pkg/genericapiserver/config.go +++ b/pkg/genericapiserver/config.go @@ -29,7 +29,6 @@ import ( "strings" "time" - "github.com/emicklei/go-restful" "github.com/go-openapi/spec" "github.com/golang/glog" "gopkg.in/natefinch/lumberjack.v2" @@ -46,6 +45,7 @@ import ( "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/cloudprovider" genericfilters "k8s.io/kubernetes/pkg/genericapiserver/filters" + "k8s.io/kubernetes/pkg/genericapiserver/mux" "k8s.io/kubernetes/pkg/genericapiserver/openapi/common" "k8s.io/kubernetes/pkg/genericapiserver/options" "k8s.io/kubernetes/pkg/genericapiserver/routes" @@ -94,9 +94,6 @@ type Config struct { // Required, the interface for serializing and converting objects to and from the wire Serializer runtime.NegotiatedSerializer - // If specified, all web services will be registered into this container - RestfulContainer *restful.Container - // If specified, requests will be allocated a random timeout between this value, and twice this value. // Note that it is up to the request handlers to ignore or honor this timeout. In seconds. MinRequestTimeout int @@ -152,6 +149,8 @@ type Config struct { // Port names should align with ports defined in ExtraServicePorts ExtraEndpointPorts []api.EndpointPort + // If non-zero, the "kubernetes" services uses this port as NodePort. + // TODO(sttts): move into master KubernetesServiceNodePort int // EnableOpenAPISupport enables OpenAPI support. Allow downstream customers to disable OpenAPI spec. @@ -173,6 +172,9 @@ type Config struct { // Predicate which is true for paths of long-running http requests LongRunningFunc genericfilters.LongRunningRequestCheck + + // Build the handler chains by decorating the apiHandler. + BuildHandlerChainsFunc func(apiHandler http.Handler, c *Config) (secure, insecure http.Handler) } type ServingInfo struct { @@ -323,6 +325,9 @@ func (c *Config) Complete() completedConfig { } c.ExternalHost = hostAndPort } + if c.BuildHandlerChainsFunc == nil { + c.BuildHandlerChainsFunc = DefaultBuildHandlerChain + } return completedConfig{c} } @@ -391,15 +396,7 @@ func (c completedConfig) New() (*GenericAPIServer, error) { openAPIDefinitions: c.OpenAPIDefinitions, } - if c.RestfulContainer != nil { - s.HandlerContainer = c.RestfulContainer - } else { - s.HandlerContainer = NewHandlerContainer(http.NewServeMux(), c.Serializer) - } - // Use CurlyRouter to be able to use regular expressions in paths. Regular expressions are required in paths for example for proxy (where the path is proxy/{kind}/{name}/{*}) - s.HandlerContainer.Router(restful.CurlyRouter{}) - s.Mux = apiserver.NewPathRecorderMux(s.HandlerContainer.ServeMux) - apiserver.InstallServiceErrorHandler(s.Serializer, s.HandlerContainer) + s.HandlerContainer = mux.NewAPIContainer(http.NewServeMux(), c.Serializer) if c.ProxyDialer != nil || c.ProxyTLSClientConfig != nil { s.ProxyTransport = utilnet.SetTransportDefaults(&http.Transport{ @@ -409,50 +406,50 @@ func (c completedConfig) New() (*GenericAPIServer, error) { } s.installAPI(c.Config) - s.Handler, s.InsecureHandler = s.buildHandlerChains(c.Config, http.Handler(s.Mux.BaseMux().(*http.ServeMux))) + + s.Handler, s.InsecureHandler = c.BuildHandlerChainsFunc(s.HandlerContainer.ServeMux, c.Config) return s, nil } -func (s *GenericAPIServer) buildHandlerChains(c *Config, handler http.Handler) (secure http.Handler, insecure http.Handler) { - // filters which insecure and secure have in common - handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") - - // insecure filters - insecure = handler - insecure = genericfilters.WithPanicRecovery(insecure, c.RequestContextMapper) - insecure = apiserverfilters.WithRequestInfo(insecure, NewRequestInfoResolver(c), c.RequestContextMapper) - insecure = api.WithRequestContext(insecure, c.RequestContextMapper) - insecure = genericfilters.WithTimeoutForNonLongRunningRequests(insecure, c.LongRunningFunc) - - // secure filters +func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) (secure, insecure http.Handler) { attributeGetter := apiserverfilters.NewRequestAttributeGetter(c.RequestContextMapper) - secure = handler - secure = apiserverfilters.WithAuthorization(secure, attributeGetter, c.Authorizer) - secure = apiserverfilters.WithImpersonation(secure, c.RequestContextMapper, c.Authorizer) - secure = apiserverfilters.WithAudit(secure, attributeGetter, c.AuditWriter) // before impersonation to read original user - secure = authhandlers.WithAuthentication(secure, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) - secure = genericfilters.WithPanicRecovery(secure, c.RequestContextMapper) - secure = apiserverfilters.WithRequestInfo(secure, NewRequestInfoResolver(c), c.RequestContextMapper) - secure = api.WithRequestContext(secure, c.RequestContextMapper) - secure = genericfilters.WithTimeoutForNonLongRunningRequests(secure, c.LongRunningFunc) - secure = genericfilters.WithMaxInFlightLimit(secure, c.MaxRequestsInFlight, c.LongRunningFunc) - return + generic := func(handler http.Handler) http.Handler { + handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true") + handler = genericfilters.WithPanicRecovery(handler, c.RequestContextMapper) + handler = apiserverfilters.WithRequestInfo(handler, NewRequestInfoResolver(c), c.RequestContextMapper) + handler = api.WithRequestContext(handler, c.RequestContextMapper) + handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc) + handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.LongRunningFunc) + return handler + } + audit := func(handler http.Handler) http.Handler { + return apiserverfilters.WithAudit(handler, attributeGetter, c.AuditWriter) + } + protect := func(handler http.Handler) http.Handler { + handler = apiserverfilters.WithAuthorization(handler, attributeGetter, c.Authorizer) + handler = apiserverfilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer) + handler = audit(handler) // before impersonation to read original user + handler = authhandlers.WithAuthentication(handler, c.RequestContextMapper, c.Authenticator, authhandlers.Unauthorized(c.SupportsBasicAuth)) + return handler + } + + return generic(protect(apiHandler)), generic(audit(apiHandler)) } func (s *GenericAPIServer) installAPI(c *Config) { if c.EnableIndex { - routes.Index{}.Install(s.Mux, s.HandlerContainer) + routes.Index{}.Install(s.HandlerContainer) } if c.EnableSwaggerSupport && c.EnableSwaggerUI { - routes.SwaggerUI{}.Install(s.Mux, s.HandlerContainer) + routes.SwaggerUI{}.Install(s.HandlerContainer) } if c.EnableProfiling { - routes.Profiling{}.Install(s.Mux, s.HandlerContainer) + routes.Profiling{}.Install(s.HandlerContainer) } if c.EnableVersion { - routes.Version{}.Install(s.Mux, s.HandlerContainer) + routes.Version{}.Install(s.HandlerContainer) } s.HandlerContainer.Add(s.DynamicApisDiscovery()) } diff --git a/pkg/genericapiserver/genericapiserver.go b/pkg/genericapiserver/genericapiserver.go index a592eeff67c..2e69f2a7c38 100644 --- a/pkg/genericapiserver/genericapiserver.go +++ b/pkg/genericapiserver/genericapiserver.go @@ -42,6 +42,7 @@ import ( "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apiserver" "k8s.io/kubernetes/pkg/client/restclient" + genericmux "k8s.io/kubernetes/pkg/genericapiserver/mux" "k8s.io/kubernetes/pkg/genericapiserver/openapi" "k8s.io/kubernetes/pkg/genericapiserver/openapi/common" "k8s.io/kubernetes/pkg/runtime" @@ -118,9 +119,8 @@ type GenericAPIServer struct { // requestContextMapper provides a way to get the context for a request. It may be nil. requestContextMapper api.RequestContextMapper - Mux *apiserver.PathRecorderMux - HandlerContainer *restful.Container - MasterCount int + // The registered APIs + HandlerContainer *genericmux.APIContainer SecureServingInfo *ServingInfo InsecureServingInfo *ServingInfo @@ -128,13 +128,9 @@ type GenericAPIServer struct { // ExternalAddress is the address (hostname or IP and port) that should be used in // external (public internet) URLs for this GenericAPIServer. ExternalAddress string + // ClusterIP is the IP address of the GenericAPIServer within the cluster. - ClusterIP net.IP - PublicReadWritePort int - ServiceReadWriteIP net.IP - ServiceReadWritePort int - ExtraServicePorts []api.ServicePort - ExtraEndpointPorts []api.EndpointPort + ClusterIP net.IP // storage contains the RESTful endpoints exposed by this GenericAPIServer storage map[string]rest.Storage @@ -150,26 +146,31 @@ type GenericAPIServer struct { // Used for custom proxy dialing, and proxy TLS options ProxyTransport http.RoundTripper - KubernetesServiceNodePort int - // Map storing information about all groups to be exposed in discovery response. // The map is from name to the group. apiGroupsForDiscoveryLock sync.RWMutex apiGroupsForDiscovery map[string]unversioned.APIGroup - // See Config.$name for documentation of these flags - - enableOpenAPISupport bool - openAPIInfo spec.Info - openAPIDefaultResponse spec.Response - openAPIDefinitions *common.OpenAPIDefinitions - // PostStartHooks are each called after the server has started listening, in a separate go func for each // with no guaranteee of ordering between them. The map key is a name used for error reporting. // It may kill the process with a panic if it wishes to by returning an error postStartHooks map[string]PostStartHookFunc postStartHookLock sync.Mutex postStartHooksCalled bool + + // See Config.$name for documentation of these flags: + + enableOpenAPISupport bool + openAPIInfo spec.Info + openAPIDefaultResponse spec.Response + openAPIDefinitions *common.OpenAPIDefinitions + MasterCount int + KubernetesServiceNodePort int // TODO(sttts): move into master + PublicReadWritePort int + ServiceReadWriteIP net.IP + ServiceReadWritePort int + ExtraServicePorts []api.ServicePort + ExtraEndpointPorts []api.EndpointPort } func init() { @@ -191,33 +192,6 @@ func (s *GenericAPIServer) MinRequestTimeout() time.Duration { return s.minRequestTimeout } -// HandleWithAuth adds an http.Handler for pattern to an http.ServeMux -// Applies the same authentication and authorization (if any is configured) -// to the request is used for the GenericAPIServer's built-in endpoints. -func (s *GenericAPIServer) HandleWithAuth(pattern string, handler http.Handler) { - // TODO: Add a way for plugged-in endpoints to translate their - // URLs into attributes that an Authorizer can understand, and have - // sensible policy defaults for plugged-in endpoints. This will be different - // for generic endpoints versus REST object endpoints. - // TODO: convert to go-restful - s.Mux.Handle(pattern, handler) -} - -// HandleFuncWithAuth adds an http.Handler for pattern to an http.ServeMux -// Applies the same authentication and authorization (if any is configured) -// to the request is used for the GenericAPIServer's built-in endpoints. -func (s *GenericAPIServer) HandleFuncWithAuth(pattern string, handler func(http.ResponseWriter, *http.Request)) { - // TODO: convert to go-restful - s.Mux.HandleFunc(pattern, handler) -} - -func NewHandlerContainer(mux *http.ServeMux, s runtime.NegotiatedSerializer) *restful.Container { - container := restful.NewContainer() - container.ServeMux = mux - apiserver.InstallRecoverHandler(s, container) - return container -} - func (s *GenericAPIServer) Run() { // install APIs which depend on other APIs to be installed if s.enableSwaggerSupport { @@ -227,7 +201,7 @@ func (s *GenericAPIServer) Run() { s.InstallOpenAPI() } - if s.SecureServingInfo != nil { + if s.SecureServingInfo != nil && s.Handler != nil { secureServer := &http.Server{ Addr: s.SecureServingInfo.BindAddress, Handler: s.Handler, @@ -282,7 +256,7 @@ func (s *GenericAPIServer) Run() { }() } - if s.InsecureServingInfo != nil { + if s.InsecureServingInfo != nil && s.InsecureHandler != nil { insecureServer := &http.Server{ Addr: s.InsecureServingInfo.BindAddress, Handler: s.InsecureHandler, @@ -343,14 +317,14 @@ func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error { apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion } - if err := apiGroupVersion.InstallREST(s.HandlerContainer); err != nil { + if err := apiGroupVersion.InstallREST(s.HandlerContainer.Container); err != nil { return fmt.Errorf("Unable to setup API %v: %v", apiGroupInfo, err) } } // Install the version handler. if apiGroupInfo.IsLegacyGroup { // Add a handler at /api to enumerate the supported api versions. - apiserver.AddApiWebService(s.Serializer, s.HandlerContainer, apiPrefix, func(req *restful.Request) *unversioned.APIVersions { + apiserver.AddApiWebService(s.Serializer, s.HandlerContainer.Container, apiPrefix, func(req *restful.Request) *unversioned.APIVersions { apiVersionsForDiscovery := unversioned.APIVersions{ ServerAddressByClientCIDRs: s.getServerAddressByClientCIDRs(req.Request), Versions: apiVersions, @@ -487,7 +461,7 @@ func (s *GenericAPIServer) getSwaggerConfig() *swagger.Config { // of swagger, so that other resource types show up in the documentation. func (s *GenericAPIServer) InstallSwaggerAPI() { // Enable swagger UI and discovery API - swagger.RegisterSwaggerService(*s.getSwaggerConfig(), s.HandlerContainer) + swagger.RegisterSwaggerService(*s.getSwaggerConfig(), s.HandlerContainer.Container) } // InstallOpenAPI installs spec endpoints for each web service. @@ -508,7 +482,7 @@ func (s *GenericAPIServer) InstallOpenAPI() { Info: &info, DefaultResponse: &s.openAPIDefaultResponse, OpenAPIDefinitions: s.openAPIDefinitions, - }, s.HandlerContainer) + }, s.HandlerContainer.Container) if err != nil { glog.Fatalf("Failed to register open api spec for %v: %v", w.RootPath(), err) } @@ -521,7 +495,7 @@ func (s *GenericAPIServer) InstallOpenAPI() { Info: &s.openAPIInfo, DefaultResponse: &s.openAPIDefaultResponse, OpenAPIDefinitions: s.openAPIDefinitions, - }, s.HandlerContainer) + }, s.HandlerContainer.Container) if err != nil { glog.Fatalf("Failed to register open api spec for root: %v", err) } diff --git a/pkg/genericapiserver/genericapiserver_test.go b/pkg/genericapiserver/genericapiserver_test.go index c6f841c58a7..9e7da2dfca6 100644 --- a/pkg/genericapiserver/genericapiserver_test.go +++ b/pkg/genericapiserver/genericapiserver_test.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -32,12 +33,11 @@ import ( "k8s.io/kubernetes/pkg/api/rest" "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apis/extensions" - "k8s.io/kubernetes/pkg/apiserver" "k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/user" + genericmux "k8s.io/kubernetes/pkg/genericapiserver/mux" ipallocator "k8s.io/kubernetes/pkg/registry/core/service/ipallocator" etcdtesting "k8s.io/kubernetes/pkg/storage/etcd/testing" utilnet "k8s.io/kubernetes/pkg/util/net" @@ -138,7 +138,7 @@ func TestInstallAPIGroups(t *testing.T) { s.InstallAPIGroup(&apiGroupsInfo[i]) } - server := httptest.NewServer(s.HandlerContainer.ServeMux) + server := httptest.NewServer(s.InsecureHandler) defer server.Close() validPaths := []string{ // "/api" @@ -158,41 +158,64 @@ func TestInstallAPIGroups(t *testing.T) { } } -// TestNewHandlerContainer verifies that NewHandlerContainer uses the -// mux provided -func TestNewHandlerContainer(t *testing.T) { - assert := assert.New(t) - mux := http.NewServeMux() - container := NewHandlerContainer(mux, nil) - assert.Equal(mux, container.ServeMux, "ServerMux's do not match") -} - -// TestHandleWithAuth verifies HandleWithAuth adds the path -// to the MuxHelper.RegisteredPaths. -func TestHandleWithAuth(t *testing.T) { - etcdserver, _, assert := setUp(t) +// TestCustomHandlerChain verifies the handler chain with custom handler chain builder functions. +func TestCustomHandlerChain(t *testing.T) { + etcdserver, config, _ := setUp(t) defer etcdserver.Terminate(t) - server := &GenericAPIServer{} - server.Mux = apiserver.NewPathRecorderMux(http.NewServeMux()) - handler := func(r http.ResponseWriter, w *http.Request) { w.Write(nil) } - server.HandleWithAuth("/test", http.HandlerFunc(handler)) + var protected, called bool - assert.Contains(server.Mux.HandledPaths(), "/test", "Path not found in MuxHelper") -} + config.Serializer = api.Codecs + config.BuildHandlerChainsFunc = func(apiHandler http.Handler, c *Config) (secure, insecure http.Handler) { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + protected = true + apiHandler.ServeHTTP(w, req) + }), http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + protected = false + apiHandler.ServeHTTP(w, req) + }) + } + handler := http.HandlerFunc(func(r http.ResponseWriter, req *http.Request) { + called = true + }) -// TestHandleFuncWithAuth verifies HandleFuncWithAuth adds the path -// to the MuxHelper.RegisteredPaths. -func TestHandleFuncWithAuth(t *testing.T) { - etcdserver, _, assert := setUp(t) - defer etcdserver.Terminate(t) + s, err := config.Complete().New() + if err != nil { + t.Fatalf("Error in bringing up the server: %v", err) + } - server := &GenericAPIServer{} - server.Mux = apiserver.NewPathRecorderMux(http.NewServeMux()) - handler := func(r http.ResponseWriter, w *http.Request) { w.Write(nil) } - server.HandleFuncWithAuth("/test", handler) + s.HandlerContainer.NonSwaggerRoutes.Handle("/nonswagger", handler) + s.HandlerContainer.SecretRoutes.Handle("/secret", handler) - assert.Contains(server.Mux.HandledPaths(), "/test", "Path not found in MuxHelper") + type Test struct { + handler http.Handler + path string + protected bool + } + for i, test := range []Test{ + {s.Handler, "/nonswagger", true}, + {s.Handler, "/secret", true}, + {s.InsecureHandler, "/nonswagger", false}, + {s.InsecureHandler, "/secret", false}, + } { + protected, called = false, false + + var w io.Reader + req, err := http.NewRequest("GET", test.path, w) + if err != nil { + t.Errorf("%d: Unexpected http error: %v", i, err) + continue + } + + test.handler.ServeHTTP(httptest.NewRecorder(), req) + + if !called { + t.Errorf("%d: Expected handler to be called.", i) + } + if test.protected != protected { + t.Errorf("%d: Expected protected=%v, got protected=%v.", i, test.protected, protected) + } + } } // TestNotRestRoutesHaveAuth checks that special non-routes are behind authz/authn. @@ -267,7 +290,7 @@ func TestInstallSwaggerAPI(t *testing.T) { mux := http.NewServeMux() server := &GenericAPIServer{} - server.HandlerContainer = NewHandlerContainer(mux, nil) + server.HandlerContainer = genericmux.NewAPIContainer(mux, nil) // Ensure swagger isn't installed without the call ws := server.HandlerContainer.RegisteredWebServices() @@ -286,7 +309,7 @@ func TestInstallSwaggerAPI(t *testing.T) { // Empty externalHost verification mux = http.NewServeMux() - server.HandlerContainer = NewHandlerContainer(mux, nil) + server.HandlerContainer = genericmux.NewAPIContainer(mux, nil) server.ExternalAddress = "" server.ClusterIP = net.IPv4(10, 10, 10, 10) server.PublicReadWritePort = 1010 @@ -328,7 +351,7 @@ func TestDiscoveryAtAPIS(t *testing.T) { master, etcdserver, _, assert := newMaster(t) defer etcdserver.Terminate(t) - server := httptest.NewServer(master.HandlerContainer.ServeMux) + server := httptest.NewServer(master.InsecureHandler) groupList, err := getGroupList(server) if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/pkg/genericapiserver/mux/container.go b/pkg/genericapiserver/mux/container.go new file mode 100644 index 00000000000..7b25415a1a8 --- /dev/null +++ b/pkg/genericapiserver/mux/container.go @@ -0,0 +1,52 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mux + +import ( + "net/http" + + "github.com/emicklei/go-restful" + + "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/runtime" +) + +// APIContainer is a restful container which in addition support registering +// handlers that do not show up in swagger or in / +type APIContainer struct { + *restful.Container + NonSwaggerRoutes PathRecorderMux + SecretRoutes Mux +} + +// NewAPIContainer constructs a new container for APIs +func NewAPIContainer(mux *http.ServeMux, s runtime.NegotiatedSerializer) *APIContainer { + c := APIContainer{ + Container: restful.NewContainer(), + NonSwaggerRoutes: PathRecorderMux{ + mux: mux, + }, + SecretRoutes: mux, + } + c.Container.ServeMux = mux + c.Container.Router(restful.CurlyRouter{}) // e.g. for proxy/{kind}/{name}/{*} + + apiserver.InstallRecoverHandler(s, c.Container) + apiserver.InstallServiceErrorHandler(s, c.Container) + + return &c +} diff --git a/pkg/genericapiserver/mux/container_test.go b/pkg/genericapiserver/mux/container_test.go new file mode 100644 index 00000000000..c098cf0bb78 --- /dev/null +++ b/pkg/genericapiserver/mux/container_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mux + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewAPIContainer(t *testing.T) { + mux := http.NewServeMux() + c := NewAPIContainer(mux, nil) + assert.Equal(t, mux, c.SecretRoutes.(*http.ServeMux), "SecretRoutes 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.SecretRoutes.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") +} diff --git a/pkg/genericapiserver/mux/doc.go b/pkg/genericapiserver/mux/doc.go new file mode 100644 index 00000000000..da9fb8ed7f5 --- /dev/null +++ b/pkg/genericapiserver/mux/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package mux contains abstractions for http multiplexing of APIs. +package mux diff --git a/pkg/apiserver/mux.go b/pkg/genericapiserver/mux/pathrecorder.go similarity index 96% rename from pkg/apiserver/mux.go rename to pkg/genericapiserver/mux/pathrecorder.go index 4f0f2114ea1..4de891a7333 100644 --- a/pkg/apiserver/mux.go +++ b/pkg/genericapiserver/mux/pathrecorder.go @@ -1,5 +1,5 @@ /* -Copyright 2015 The Kubernetes Authors. +Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package apiserver +package mux import ( "net/http" diff --git a/pkg/genericapiserver/routes/index.go b/pkg/genericapiserver/routes/index.go index f3280d5b040..f4ef7e58d10 100644 --- a/pkg/genericapiserver/routes/index.go +++ b/pkg/genericapiserver/routes/index.go @@ -20,19 +20,17 @@ import ( "net/http" "sort" - "github.com/emicklei/go-restful" - "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/genericapiserver/mux" ) // Index provides a webservice for the http root / listing all known paths. type Index struct{} // Install adds the Index webservice to the given mux. -func (i Index) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { - // do not register this using restful Webservice since we do not want to surface this in api docs. - mux.BaseMux().HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { +func (i Index) Install(c *mux.APIContainer) { + c.SecretRoutes.HandleFunc("/", 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 registered. @@ -45,7 +43,7 @@ func (i Index) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { handledPaths = append(handledPaths, ws.RootPath()) } // Extract the paths handled using mux handler. - handledPaths = append(handledPaths, mux.HandledPaths()...) + handledPaths = append(handledPaths, c.NonSwaggerRoutes.HandledPaths()...) sort.Strings(handledPaths) apiserver.WriteRawJSON(status, unversioned.RootPaths{Paths: handledPaths}, w) }) diff --git a/pkg/genericapiserver/routes/profiling.go b/pkg/genericapiserver/routes/profiling.go index fd3dc369a12..120886aac31 100644 --- a/pkg/genericapiserver/routes/profiling.go +++ b/pkg/genericapiserver/routes/profiling.go @@ -17,18 +17,17 @@ limitations under the License. package routes import ( - "github.com/emicklei/go-restful" - - "k8s.io/kubernetes/pkg/apiserver" "net/http/pprof" + + "k8s.io/kubernetes/pkg/genericapiserver/mux" ) // Profiling adds handlers for pprof under /debug/pprof. type Profiling struct{} // Install adds the Profiling webservice to the given mux. -func (d Profiling) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { - mux.BaseMux().HandleFunc("/debug/pprof/", pprof.Index) - mux.BaseMux().HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.BaseMux().HandleFunc("/debug/pprof/symbol", pprof.Symbol) +func (d Profiling) Install(c *mux.APIContainer) { + c.SecretRoutes.HandleFunc("/debug/pprof/", pprof.Index) + c.SecretRoutes.HandleFunc("/debug/pprof/profile", pprof.Profile) + c.SecretRoutes.HandleFunc("/debug/pprof/symbol", pprof.Symbol) } diff --git a/pkg/genericapiserver/routes/swaggerui.go b/pkg/genericapiserver/routes/swaggerui.go index 94349f3cdd3..5551466914c 100644 --- a/pkg/genericapiserver/routes/swaggerui.go +++ b/pkg/genericapiserver/routes/swaggerui.go @@ -20,9 +20,8 @@ import ( "net/http" assetfs "github.com/elazarl/go-bindata-assetfs" - "github.com/emicklei/go-restful" - "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/genericapiserver/mux" "k8s.io/kubernetes/pkg/genericapiserver/routes/data/swagger" ) @@ -30,12 +29,12 @@ import ( type SwaggerUI struct{} // Install adds the SwaggerUI webservice to the given mux. -func (l SwaggerUI) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { +func (l SwaggerUI) Install(c *mux.APIContainer) { fileServer := http.FileServer(&assetfs.AssetFS{ Asset: swagger.Asset, AssetDir: swagger.AssetDir, Prefix: "third_party/swagger-ui", }) prefix := "/swagger-ui/" - mux.Handle(prefix, http.StripPrefix(prefix, fileServer)) + c.NonSwaggerRoutes.Handle(prefix, http.StripPrefix(prefix, fileServer)) } diff --git a/pkg/genericapiserver/routes/version.go b/pkg/genericapiserver/routes/version.go index b4ead0f27f0..d180a19fafc 100644 --- a/pkg/genericapiserver/routes/version.go +++ b/pkg/genericapiserver/routes/version.go @@ -22,6 +22,7 @@ import ( "github.com/emicklei/go-restful" "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/genericapiserver/mux" "k8s.io/kubernetes/pkg/version" ) @@ -29,7 +30,7 @@ import ( type Version struct{} // Install registers the APIServer's `/version` handler. -func (v Version) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { +func (v Version) Install(c *mux.APIContainer) { // Set up a service to return the git code version. versionWS := new(restful.WebService) versionWS.Path("/version") diff --git a/pkg/master/master.go b/pkg/master/master.go index f1737e22066..4936bcf433f 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -192,10 +192,10 @@ func (c completedConfig) New() (*Master, error) { } if c.EnableUISupport { - routes.UIRedirect{}.Install(s.Mux, s.HandlerContainer) + routes.UIRedirect{}.Install(s.HandlerContainer) } if c.EnableLogsSupport { - routes.Logs{}.Install(s.Mux, s.HandlerContainer) + routes.Logs{}.Install(s.HandlerContainer) } m := &Master{ @@ -284,12 +284,12 @@ func (m *Master) InstallAPIs(c *Config) { Help: "The time since the last successful synchronization of the SSH tunnels for proxy requests.", }, func() float64 { return float64(m.tunneler.SecondsSinceSync()) }) } - healthz.InstallHandler(m.Mux, healthzChecks...) + healthz.InstallHandler(&m.HandlerContainer.NonSwaggerRoutes, healthzChecks...) if c.GenericConfig.EnableProfiling { - routes.MetricsWithReset{}.Install(m.Mux, m.HandlerContainer) + routes.MetricsWithReset{}.Install(m.HandlerContainer) } else { - routes.DefaultMetrics{}.Install(m.Mux, m.HandlerContainer) + routes.DefaultMetrics{}.Install(m.HandlerContainer) } // Install third party resource support if requested @@ -612,10 +612,10 @@ func (m *Master) InstallThirdPartyResource(rsrc *extensions.ThirdPartyResource) // the group with the new API if m.hasThirdPartyGroupStorage(path) { m.addThirdPartyResourceStorage(path, plural.Resource, thirdparty.Storage[plural.Resource].(*thirdpartyresourcedataetcd.REST), apiGroup) - return thirdparty.UpdateREST(m.HandlerContainer) + return thirdparty.UpdateREST(m.HandlerContainer.Container) } - if err := thirdparty.InstallREST(m.HandlerContainer); err != nil { + if err := thirdparty.InstallREST(m.HandlerContainer.Container); err != nil { glog.Errorf("Unable to setup thirdparty api: %v", err) } m.HandlerContainer.Add(apiserver.NewGroupWebService(api.Codecs, path, apiGroup)) diff --git a/pkg/routes/logs.go b/pkg/routes/logs.go index 07d845a5419..bbe5512a87d 100644 --- a/pkg/routes/logs.go +++ b/pkg/routes/logs.go @@ -22,13 +22,13 @@ import ( "github.com/emicklei/go-restful" - "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/genericapiserver/mux" ) // Logs adds handlers for the /logs path serving log files from /var/log. type Logs struct{} -func (l Logs) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { +func (l Logs) Install(c *mux.APIContainer) { // use restful: ws.Route(ws.GET("/logs/{logpath:*}").To(fileHandler)) // See github.com/emicklei/go-restful/blob/master/examples/restful-serve-static.go ws := new(restful.WebService) diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go index c176d11da66..ddf7a82a2a2 100644 --- a/pkg/routes/metrics.go +++ b/pkg/routes/metrics.go @@ -20,28 +20,27 @@ import ( "io" "net/http" - "k8s.io/kubernetes/pkg/apiserver" apiservermetrics "k8s.io/kubernetes/pkg/apiserver/metrics" + "k8s.io/kubernetes/pkg/genericapiserver/mux" etcdmetrics "k8s.io/kubernetes/pkg/storage/etcd/metrics" - "github.com/emicklei/go-restful" "github.com/prometheus/client_golang/prometheus" ) // DefaultMetrics installs the default prometheus metrics handler type DefaultMetrics struct{} -func (m DefaultMetrics) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { - mux.HandleFunc("/metrics", prometheus.Handler().ServeHTTP) +func (m DefaultMetrics) Install(c *mux.APIContainer) { + c.NonSwaggerRoutes.Handle("/metrics", prometheus.Handler()) } // MetricsWithReset install the prometheus metrics handler extended with support for the DELETE method // which resets the metrics. type MetricsWithReset struct{} -func (m MetricsWithReset) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { +func (m MetricsWithReset) Install(c *mux.APIContainer) { defaultMetricsHandler := prometheus.Handler().ServeHTTP - mux.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) { + c.NonSwaggerRoutes.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) { if req.Method == "DELETE" { apiservermetrics.Reset() etcdmetrics.Reset() diff --git a/pkg/routes/ui.go b/pkg/routes/ui.go index 2951fc2a591..b87e1554642 100644 --- a/pkg/routes/ui.go +++ b/pkg/routes/ui.go @@ -19,9 +19,7 @@ package routes import ( "net/http" - "github.com/emicklei/go-restful" - - "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/genericapiserver/mux" ) const dashboardPath = "/api/v1/proxy/namespaces/kube-system/services/kubernetes-dashboard" @@ -29,8 +27,8 @@ const dashboardPath = "/api/v1/proxy/namespaces/kube-system/services/kubernetes- // UIRediect redirects /ui to the kube-ui proxy path. type UIRedirect struct{} -func (r UIRedirect) Install(mux *apiserver.PathRecorderMux, c *restful.Container) { - mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { +func (r UIRedirect) Install(c *mux.APIContainer) { + c.NonSwaggerRoutes.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, dashboardPath, http.StatusTemporaryRedirect) }) } diff --git a/test/integration/openshift/openshift_test.go b/test/integration/openshift/openshift_test.go index e6452da4aed..4bac356af15 100644 --- a/test/integration/openshift/openshift_test.go +++ b/test/integration/openshift/openshift_test.go @@ -29,7 +29,6 @@ func TestMasterExportsSymbols(t *testing.T) { _ = &master.Config{ GenericConfig: &genericapiserver.Config{ EnableSwaggerSupport: false, - RestfulContainer: nil, }, EnableCoreControllers: false, EnableUISupport: false,