mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
add proxying capability to kubernetes-discovery
This commit is contained in:
parent
492f8d412f
commit
b81645e382
@ -18,6 +18,8 @@ spec:
|
|||||||
image: kubernetes-discovery:latest
|
image: kubernetes-discovery:latest
|
||||||
imagePullPolicy: Never
|
imagePullPolicy: Never
|
||||||
args:
|
args:
|
||||||
|
- "--proxy-client-cert-file=/var/run/auth-proxy-client/tls.crt"
|
||||||
|
- "--proxy-client-key-file=/var/run/auth-proxy-client/tls.key"
|
||||||
- "--tls-cert-file=/var/run/serving-cert/tls.crt"
|
- "--tls-cert-file=/var/run/serving-cert/tls.crt"
|
||||||
- "--tls-private-key-file=/var/run/serving-cert/tls.key"
|
- "--tls-private-key-file=/var/run/serving-cert/tls.key"
|
||||||
- "--tls-ca-file=/var/run/serving-ca/ca.crt"
|
- "--tls-ca-file=/var/run/serving-ca/ca.crt"
|
||||||
|
@ -16,6 +16,7 @@ go_library(
|
|||||||
"apiserver.go",
|
"apiserver.go",
|
||||||
"apiservice_controller.go",
|
"apiservice_controller.go",
|
||||||
"handler_apis.go",
|
"handler_apis.go",
|
||||||
|
"handler_proxy.go",
|
||||||
],
|
],
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
deps = [
|
deps = [
|
||||||
@ -35,14 +36,17 @@ go_library(
|
|||||||
"//pkg/apiserver/filters:go_default_library",
|
"//pkg/apiserver/filters:go_default_library",
|
||||||
"//pkg/auth/handlers:go_default_library",
|
"//pkg/auth/handlers:go_default_library",
|
||||||
"//pkg/client/cache:go_default_library",
|
"//pkg/client/cache:go_default_library",
|
||||||
|
"//pkg/client/restclient:go_default_library",
|
||||||
|
"//pkg/client/transport:go_default_library",
|
||||||
"//pkg/controller:go_default_library",
|
"//pkg/controller:go_default_library",
|
||||||
"//pkg/genericapiserver:go_default_library",
|
"//pkg/genericapiserver:go_default_library",
|
||||||
"//pkg/genericapiserver/filters:go_default_library",
|
"//pkg/genericapiserver/filters:go_default_library",
|
||||||
"//pkg/labels:go_default_library",
|
"//pkg/labels:go_default_library",
|
||||||
"//pkg/registry/generic:go_default_library",
|
"//pkg/registry/generic:go_default_library",
|
||||||
|
"//pkg/registry/generic/rest:go_default_library",
|
||||||
"//pkg/runtime:go_default_library",
|
"//pkg/runtime:go_default_library",
|
||||||
|
"//pkg/util/httpstream/spdy:go_default_library",
|
||||||
"//pkg/util/runtime:go_default_library",
|
"//pkg/util/runtime:go_default_library",
|
||||||
"//pkg/util/sets:go_default_library",
|
|
||||||
"//pkg/util/wait:go_default_library",
|
"//pkg/util/wait:go_default_library",
|
||||||
"//pkg/util/workqueue:go_default_library",
|
"//pkg/util/workqueue:go_default_library",
|
||||||
"//pkg/version:go_default_library",
|
"//pkg/version:go_default_library",
|
||||||
@ -52,7 +56,10 @@ go_library(
|
|||||||
|
|
||||||
go_test(
|
go_test(
|
||||||
name = "go_default_test",
|
name = "go_default_test",
|
||||||
srcs = ["handler_apis_test.go"],
|
srcs = [
|
||||||
|
"handler_apis_test.go",
|
||||||
|
"handler_proxy_test.go",
|
||||||
|
],
|
||||||
library = "go_default_library",
|
library = "go_default_library",
|
||||||
tags = ["automanaged"],
|
tags = ["automanaged"],
|
||||||
deps = [
|
deps = [
|
||||||
@ -60,8 +67,11 @@ go_test(
|
|||||||
"//cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion:go_default_library",
|
"//cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion:go_default_library",
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/apis/meta/v1:go_default_library",
|
"//pkg/apis/meta/v1:go_default_library",
|
||||||
|
"//pkg/apiserver/request:go_default_library",
|
||||||
|
"//pkg/auth/user:go_default_library",
|
||||||
"//pkg/client/cache:go_default_library",
|
"//pkg/client/cache:go_default_library",
|
||||||
"//pkg/runtime:go_default_library",
|
"//pkg/runtime:go_default_library",
|
||||||
"//pkg/util/diff:go_default_library",
|
"//pkg/util/diff:go_default_library",
|
||||||
|
"//pkg/util/sets:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -28,7 +28,6 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/genericapiserver"
|
"k8s.io/kubernetes/pkg/genericapiserver"
|
||||||
genericfilters "k8s.io/kubernetes/pkg/genericapiserver/filters"
|
genericfilters "k8s.io/kubernetes/pkg/genericapiserver/filters"
|
||||||
"k8s.io/kubernetes/pkg/registry/generic"
|
"k8s.io/kubernetes/pkg/registry/generic"
|
||||||
"k8s.io/kubernetes/pkg/util/sets"
|
|
||||||
"k8s.io/kubernetes/pkg/util/wait"
|
"k8s.io/kubernetes/pkg/util/wait"
|
||||||
"k8s.io/kubernetes/pkg/version"
|
"k8s.io/kubernetes/pkg/version"
|
||||||
|
|
||||||
@ -47,6 +46,11 @@ const legacyAPIServiceName = "v1."
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
GenericConfig *genericapiserver.Config
|
GenericConfig *genericapiserver.Config
|
||||||
|
|
||||||
|
// ProxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
|
||||||
|
// this to confirm the proxy's identity
|
||||||
|
ProxyClientCert []byte
|
||||||
|
ProxyClientKey []byte
|
||||||
|
|
||||||
// RESTOptionsGetter is used to construct storage for a particular resource
|
// RESTOptionsGetter is used to construct storage for a particular resource
|
||||||
RESTOptionsGetter generic.RESTOptionsGetter
|
RESTOptionsGetter generic.RESTOptionsGetter
|
||||||
}
|
}
|
||||||
@ -55,13 +59,22 @@ type Config struct {
|
|||||||
type APIDiscoveryServer struct {
|
type APIDiscoveryServer struct {
|
||||||
GenericAPIServer *genericapiserver.GenericAPIServer
|
GenericAPIServer *genericapiserver.GenericAPIServer
|
||||||
|
|
||||||
// handledAPIServices tracks which APIServices have already been handled. Once endpoints are added,
|
contextMapper api.RequestContextMapper
|
||||||
// the listers that are used keep bits in sync automatically.
|
|
||||||
handledAPIServices sets.String
|
// proxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
|
||||||
|
// this to confirm the proxy's identity
|
||||||
|
proxyClientCert []byte
|
||||||
|
proxyClientKey []byte
|
||||||
|
|
||||||
|
// proxyHandlers are the proxy handlers that are currently registered, keyed by apiservice.name
|
||||||
|
proxyHandlers map[string]*proxyHandler
|
||||||
|
|
||||||
// lister is used to add group handling for /apis/<group> discovery lookups based on
|
// lister is used to add group handling for /apis/<group> discovery lookups based on
|
||||||
// controller state
|
// controller state
|
||||||
lister listers.APIServiceLister
|
lister listers.APIServiceLister
|
||||||
|
|
||||||
|
// proxyMux intercepts requests that need to be proxied to backing API servers
|
||||||
|
proxyMux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
type completedConfig struct {
|
type completedConfig struct {
|
||||||
@ -91,9 +104,12 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
|
|||||||
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
|
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proxyMux := http.NewServeMux()
|
||||||
|
|
||||||
// most API servers don't need to do this, but we need a custom handler chain to handle the special /apis handling here
|
// most API servers don't need to do this, but we need a custom handler chain to handle the special /apis handling here
|
||||||
c.Config.GenericConfig.BuildHandlerChainsFunc = (&handlerChainConfig{
|
c.Config.GenericConfig.BuildHandlerChainsFunc = (&handlerChainConfig{
|
||||||
informers: informerFactory,
|
informers: informerFactory,
|
||||||
|
proxyMux: proxyMux,
|
||||||
}).handlerChain
|
}).handlerChain
|
||||||
|
|
||||||
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
|
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
|
||||||
@ -102,9 +118,13 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := &APIDiscoveryServer{
|
s := &APIDiscoveryServer{
|
||||||
GenericAPIServer: genericServer,
|
GenericAPIServer: genericServer,
|
||||||
handledAPIServices: sets.String{},
|
contextMapper: c.GenericConfig.RequestContextMapper,
|
||||||
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
|
proxyClientCert: c.ProxyClientCert,
|
||||||
|
proxyClientKey: c.ProxyClientKey,
|
||||||
|
proxyHandlers: map[string]*proxyHandler{},
|
||||||
|
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
|
||||||
|
proxyMux: proxyMux,
|
||||||
}
|
}
|
||||||
|
|
||||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiregistration.GroupName)
|
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiregistration.GroupName)
|
||||||
@ -134,6 +154,7 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
|
|||||||
// handlerChainConfig is the config used to build the custom handler chain for this api server
|
// handlerChainConfig is the config used to build the custom handler chain for this api server
|
||||||
type handlerChainConfig struct {
|
type handlerChainConfig struct {
|
||||||
informers informers.SharedInformerFactory
|
informers informers.SharedInformerFactory
|
||||||
|
proxyMux *http.ServeMux
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlerChain is a method to build the handler chain for this API server. We need a custom handler chain so that we
|
// handlerChain is a method to build the handler chain for this API server. We need a custom handler chain so that we
|
||||||
@ -144,6 +165,12 @@ func (h *handlerChainConfig) handlerChain(apiHandler http.Handler, c *genericapi
|
|||||||
handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices())
|
handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices())
|
||||||
|
|
||||||
handler = apiserverfilters.WithAuthorization(handler, c.RequestContextMapper, c.Authorizer)
|
handler = apiserverfilters.WithAuthorization(handler, c.RequestContextMapper, c.Authorizer)
|
||||||
|
|
||||||
|
// this mux is NOT protected by authorization, but DOES have authentication information
|
||||||
|
// this is so that everyone can hit the proxy and we can properly identify the user. The backing
|
||||||
|
// API server will deal with authorization
|
||||||
|
handler = WithProxyMux(handler, h.proxyMux)
|
||||||
|
|
||||||
handler = apiserverfilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer)
|
handler = apiserverfilters.WithImpersonation(handler, c.RequestContextMapper, c.Authorizer)
|
||||||
// audit to stdout to help with debugging as we get this started
|
// audit to stdout to help with debugging as we get this started
|
||||||
handler = apiserverfilters.WithAudit(handler, c.RequestContextMapper, os.Stdout)
|
handler = apiserverfilters.WithAudit(handler, c.RequestContextMapper, os.Stdout)
|
||||||
@ -162,12 +189,34 @@ func (h *handlerChainConfig) handlerChain(apiHandler http.Handler, c *genericapi
|
|||||||
// AddAPIService adds an API service. It is not thread-safe, so only call it on one thread at a time please.
|
// AddAPIService adds an API service. It is not thread-safe, so only call it on one thread at a time please.
|
||||||
// It's a slow moving API, so its ok to run the controller on a single thread
|
// It's a slow moving API, so its ok to run the controller on a single thread
|
||||||
func (s *APIDiscoveryServer) AddAPIService(apiService *apiregistration.APIService) {
|
func (s *APIDiscoveryServer) AddAPIService(apiService *apiregistration.APIService) {
|
||||||
if s.handledAPIServices.Has(apiService.Name) {
|
// if the proxyHandler already exists, it needs to be updated. The discovery bits do not
|
||||||
|
// since they are wired against listers because they require multiple resources to respond
|
||||||
|
if proxyHandler, exists := s.proxyHandlers[apiService.Name]; exists {
|
||||||
|
proxyHandler.updateAPIService(apiService)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
|
||||||
|
// v1. is a special case for the legacy API. It proxies to a wider set of endpoints.
|
||||||
|
if apiService.Name == "v1." {
|
||||||
|
proxyPath = "/api"
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the proxy handler
|
||||||
|
proxyHandler := &proxyHandler{
|
||||||
|
contextMapper: s.contextMapper,
|
||||||
|
proxyClientCert: s.proxyClientCert,
|
||||||
|
proxyClientKey: s.proxyClientKey,
|
||||||
|
transportBuildingError: nil,
|
||||||
|
proxyRoundTripper: nil,
|
||||||
|
}
|
||||||
|
proxyHandler.updateAPIService(apiService)
|
||||||
|
s.proxyHandlers[apiService.Name] = proxyHandler
|
||||||
|
s.proxyMux.Handle(proxyPath, proxyHandler)
|
||||||
|
s.proxyMux.Handle(proxyPath+"/", proxyHandler)
|
||||||
|
|
||||||
// if we're dealing with the legacy group, we're done here
|
// if we're dealing with the legacy group, we're done here
|
||||||
if apiService.Name == legacyAPIServiceName {
|
if apiService.Name == legacyAPIServiceName {
|
||||||
s.handledAPIServices.Insert(apiService.Name)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +235,23 @@ func (s *APIDiscoveryServer) AddAPIService(apiService *apiregistration.APIServic
|
|||||||
// RemoveAPIService removes the APIService from being handled. Later on it will disable the proxy endpoint.
|
// RemoveAPIService removes the APIService from being handled. Later on it will disable the proxy endpoint.
|
||||||
// Right now it does nothing because our handler has to properly 404 itself since muxes don't unregister
|
// Right now it does nothing because our handler has to properly 404 itself since muxes don't unregister
|
||||||
func (s *APIDiscoveryServer) RemoveAPIService(apiServiceName string) {
|
func (s *APIDiscoveryServer) RemoveAPIService(apiServiceName string) {
|
||||||
if !s.handledAPIServices.Has(apiServiceName) {
|
proxyHandler, exists := s.proxyHandlers[apiServiceName]
|
||||||
|
if !exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
proxyHandler.removeAPIService()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithProxyMux(handler http.Handler, mux *http.ServeMux) http.Handler {
|
||||||
|
if mux == nil {
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// register the handler at this stage against everything under slash. More specific paths that get registered will take precedence
|
||||||
|
// this effectively delegates by default unless something specific gets registered.
|
||||||
|
mux.Handle("/", handler)
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
mux.ServeHTTP(w, req)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ func WithAPIs(handler http.Handler, informer informers.APIServiceInformer) http.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// apisHandler servers the `/apis` endpoint.
|
// apisHandler serves the `/apis` endpoint.
|
||||||
// This is registered as a filter so that it never collides with any explictly registered endpoints
|
// This is registered as a filter so that it never collides with any explictly registered endpoints
|
||||||
type apisHandler struct {
|
type apisHandler struct {
|
||||||
lister listers.APIServiceLister
|
lister listers.APIServiceLister
|
||||||
@ -79,7 +79,8 @@ func (r *apisHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
discoveryGroupList := &metav1.APIGroupList{
|
discoveryGroupList := &metav1.APIGroupList{
|
||||||
// always add OUR api group to the list first
|
// always add OUR api group to the list first. Since we'll never have a registered APIService for it
|
||||||
|
// and since this is the crux of the API, having this first will give our names priority. It's good to be king.
|
||||||
Groups: []metav1.APIGroup{discoveryGroup},
|
Groups: []metav1.APIGroup{discoveryGroup},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +131,7 @@ func newDiscoveryAPIGroup(apiServices []*apiregistrationapi.APIService) *metav1.
|
|||||||
return discoveryGroup
|
return discoveryGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiGroupHandler servers the `/apis/<group>` endpoint.
|
// apiGroupHandler serves the `/apis/<group>` endpoint.
|
||||||
type apiGroupHandler struct {
|
type apiGroupHandler struct {
|
||||||
groupName string
|
groupName string
|
||||||
|
|
||||||
|
198
cmd/kubernetes-discovery/pkg/apiserver/handler_proxy.go
Normal file
198
cmd/kubernetes-discovery/pkg/apiserver/handler_proxy.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
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 apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/apiserver"
|
||||||
|
"k8s.io/kubernetes/pkg/client/restclient"
|
||||||
|
"k8s.io/kubernetes/pkg/client/transport"
|
||||||
|
genericrest "k8s.io/kubernetes/pkg/registry/generic/rest"
|
||||||
|
"k8s.io/kubernetes/pkg/runtime"
|
||||||
|
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
||||||
|
|
||||||
|
apiregistrationapi "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
|
||||||
|
)
|
||||||
|
|
||||||
|
// proxyHandler provides a http.Handler which will proxy traffic to locations
|
||||||
|
// specified by items implementing Redirector.
|
||||||
|
type proxyHandler struct {
|
||||||
|
contextMapper api.RequestContextMapper
|
||||||
|
|
||||||
|
// proxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
|
||||||
|
// this to confirm the proxy's identity
|
||||||
|
proxyClientCert []byte
|
||||||
|
proxyClientKey []byte
|
||||||
|
|
||||||
|
// lock protects us for updates.
|
||||||
|
lock sync.RWMutex
|
||||||
|
// restConfig holds the information for building a roundtripper
|
||||||
|
restConfig *restclient.Config
|
||||||
|
// transportBuildingError is an error produced while building the transport. If this
|
||||||
|
// is non-nil, it will be reported to clients.
|
||||||
|
transportBuildingError error
|
||||||
|
// proxyRoundTripper is the re-useable portion of the transport. It does not vary with any request.
|
||||||
|
proxyRoundTripper http.RoundTripper
|
||||||
|
// destinationHost is the hostname of the backing API server
|
||||||
|
destinationHost string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
proxyRoundTripper, err := r.getRoundTripper()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if proxyRoundTripper == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := r.contextMapper.Get(req)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "missing context", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, ok := api.UserFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "missing user", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a new location based on the existing request pointed at the target service
|
||||||
|
location := &url.URL{}
|
||||||
|
location.Scheme = "https"
|
||||||
|
location.Host = r.getDestinationHost()
|
||||||
|
location.Path = req.URL.Path
|
||||||
|
location.RawQuery = req.URL.Query().Encode()
|
||||||
|
|
||||||
|
// make a new request object with the updated location and the body we already have
|
||||||
|
newReq, err := http.NewRequest(req.Method, location.String(), req.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mergeHeader(newReq.Header, req.Header)
|
||||||
|
newReq.ContentLength = req.ContentLength
|
||||||
|
// Copy the TransferEncoding is for future-proofing. Currently Go only supports "chunked" and
|
||||||
|
// it can determine the TransferEncoding based on ContentLength and the Body.
|
||||||
|
newReq.TransferEncoding = req.TransferEncoding
|
||||||
|
|
||||||
|
upgrade := false
|
||||||
|
// we need to wrap the roundtripper in another roundtripper which will apply the front proxy headers
|
||||||
|
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)
|
||||||
|
proxyRoundTripper, upgrade, err = r.maybeWrapForConnectionUpgrades(proxyRoundTripper, req)
|
||||||
|
|
||||||
|
handler := genericrest.NewUpgradeAwareProxyHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
|
||||||
|
handler.ServeHTTP(w, newReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeWrapForConnectionUpgrades wraps the roundtripper for upgrades. The bool indicates if it was wrapped
|
||||||
|
func (r *proxyHandler) maybeWrapForConnectionUpgrades(rt http.RoundTripper, req *http.Request) (http.RoundTripper, bool, error) {
|
||||||
|
connectionHeader := req.Header.Get("Connection")
|
||||||
|
if len(connectionHeader) == 0 {
|
||||||
|
return rt, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := r.getRESTConfig()
|
||||||
|
tlsConfig, err := restclient.TLSConfigFor(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, true, err
|
||||||
|
}
|
||||||
|
upgradeRoundTripper := spdy.NewRoundTripper(tlsConfig)
|
||||||
|
wrappedRT, err := restclient.HTTPWrappersForConfig(cfg, upgradeRoundTripper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrappedRT, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeHeader(dst, src http.Header) {
|
||||||
|
for k, vv := range src {
|
||||||
|
for _, v := range vv {
|
||||||
|
dst.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// responder implements rest.Responder for assisting a connector in writing objects or errors.
|
||||||
|
type responder struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this should properly handle content type negotiation
|
||||||
|
// if the caller asked for protobuf and you write JSON bad things happen.
|
||||||
|
func (r *responder) Object(statusCode int, obj runtime.Object) {
|
||||||
|
apiserver.WriteRawJSON(statusCode, obj, r.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *responder) Error(err error) {
|
||||||
|
http.Error(r.w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// these methods provide locked access to fields
|
||||||
|
|
||||||
|
func (r *proxyHandler) updateAPIService(apiService *apiregistrationapi.APIService) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
r.transportBuildingError = nil
|
||||||
|
r.proxyRoundTripper = nil
|
||||||
|
|
||||||
|
r.destinationHost = apiService.Spec.Service.Name + "." + apiService.Spec.Service.Namespace + ".svc"
|
||||||
|
r.restConfig = &restclient.Config{
|
||||||
|
Insecure: apiService.Spec.InsecureSkipTLSVerify,
|
||||||
|
TLSClientConfig: restclient.TLSClientConfig{
|
||||||
|
CertData: r.proxyClientCert,
|
||||||
|
KeyData: r.proxyClientKey,
|
||||||
|
CAData: apiService.Spec.CABundle,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.proxyRoundTripper, r.transportBuildingError = restclient.TransportFor(r.restConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *proxyHandler) removeAPIService() {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
r.transportBuildingError = nil
|
||||||
|
r.proxyRoundTripper = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *proxyHandler) getRoundTripper() (http.RoundTripper, error) {
|
||||||
|
r.lock.RLock()
|
||||||
|
defer r.lock.RUnlock()
|
||||||
|
|
||||||
|
return r.proxyRoundTripper, r.transportBuildingError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *proxyHandler) getDestinationHost() string {
|
||||||
|
r.lock.RLock()
|
||||||
|
defer r.lock.RUnlock()
|
||||||
|
return r.destinationHost
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *proxyHandler) getRESTConfig() *restclient.Config {
|
||||||
|
r.lock.RLock()
|
||||||
|
defer r.lock.RUnlock()
|
||||||
|
return r.restConfig
|
||||||
|
}
|
198
cmd/kubernetes-discovery/pkg/apiserver/handler_proxy_test.go
Normal file
198
cmd/kubernetes-discovery/pkg/apiserver/handler_proxy_test.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
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 apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/apiserver/request"
|
||||||
|
"k8s.io/kubernetes/pkg/auth/user"
|
||||||
|
"k8s.io/kubernetes/pkg/util/sets"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
|
||||||
|
)
|
||||||
|
|
||||||
|
type targetHTTPHandler struct {
|
||||||
|
called bool
|
||||||
|
headers map[string][]string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *targetHTTPHandler) Reset() {
|
||||||
|
d.path = ""
|
||||||
|
d.called = false
|
||||||
|
d.headers = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *targetHTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
d.path = r.URL.Path
|
||||||
|
d.called = true
|
||||||
|
d.headers = r.Header
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRequestContextMapper struct {
|
||||||
|
user user.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *fakeRequestContextMapper) Get(req *http.Request) (api.Context, bool) {
|
||||||
|
ctx := api.NewContext()
|
||||||
|
if m.user != nil {
|
||||||
|
ctx = api.WithUser(ctx, m.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := &request.RequestInfoFactory{
|
||||||
|
APIPrefixes: sets.NewString("api", "apis"),
|
||||||
|
GrouplessAPIPrefixes: sets.NewString("api"),
|
||||||
|
}
|
||||||
|
info, err := resolver.NewRequestInfo(req)
|
||||||
|
if err == nil {
|
||||||
|
ctx = request.WithRequestInfo(ctx, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*fakeRequestContextMapper) Update(req *http.Request, context api.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProxyHandler(t *testing.T) {
|
||||||
|
target := &targetHTTPHandler{}
|
||||||
|
targetServer := httptest.NewTLSServer(target)
|
||||||
|
defer targetServer.Close()
|
||||||
|
|
||||||
|
handler := &proxyHandler{}
|
||||||
|
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
user user.Info
|
||||||
|
path string
|
||||||
|
apiService *apiregistration.APIService
|
||||||
|
|
||||||
|
expectedStatusCode int
|
||||||
|
expectedBody string
|
||||||
|
expectedCalled bool
|
||||||
|
expectedHeaders map[string][]string
|
||||||
|
}{
|
||||||
|
"no target": {
|
||||||
|
expectedStatusCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
"no user": {
|
||||||
|
apiService: &apiregistration.APIService{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
|
||||||
|
Spec: apiregistration.APIServiceSpec{
|
||||||
|
Group: "foo",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusInternalServerError,
|
||||||
|
expectedBody: "missing user",
|
||||||
|
},
|
||||||
|
"proxy with user": {
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "username",
|
||||||
|
Groups: []string{"one", "two"},
|
||||||
|
},
|
||||||
|
path: "/request/path",
|
||||||
|
apiService: &apiregistration.APIService{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
|
||||||
|
Spec: apiregistration.APIServiceSpec{
|
||||||
|
Group: "foo",
|
||||||
|
Version: "v1",
|
||||||
|
InsecureSkipTLSVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedCalled: true,
|
||||||
|
expectedHeaders: map[string][]string{
|
||||||
|
"X-Forwarded-Proto": {"https"},
|
||||||
|
"X-Forwarded-Uri": {"/request/path"},
|
||||||
|
"X-Remote-User": {"username"},
|
||||||
|
"User-Agent": {"Go-http-client/1.1"},
|
||||||
|
"Accept-Encoding": {"gzip"},
|
||||||
|
"X-Remote-Group": {"one", "two"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"fail on bad serving cert": {
|
||||||
|
user: &user.DefaultInfo{
|
||||||
|
Name: "username",
|
||||||
|
Groups: []string{"one", "two"},
|
||||||
|
},
|
||||||
|
path: "/request/path",
|
||||||
|
apiService: &apiregistration.APIService{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
|
||||||
|
Spec: apiregistration.APIServiceSpec{
|
||||||
|
Group: "foo",
|
||||||
|
Version: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
target.Reset()
|
||||||
|
handler.contextMapper = &fakeRequestContextMapper{user: tc.user}
|
||||||
|
handler.removeAPIService()
|
||||||
|
if tc.apiService != nil {
|
||||||
|
handler.updateAPIService(tc.apiService)
|
||||||
|
handler.destinationHost = targetServer.Listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(server.URL + tc.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e, a := tc.expectedStatusCode, resp.StatusCode; e != a {
|
||||||
|
body, _ := httputil.DumpResponse(resp, true)
|
||||||
|
t.Logf("%s: %v", name, string(body))
|
||||||
|
t.Errorf("%s: expected %v, got %v", name, e, a)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(bytes), tc.expectedBody) {
|
||||||
|
t.Errorf("%s: expected %q, got %q", name, tc.expectedBody, string(bytes))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, a := tc.expectedCalled, target.called; e != a {
|
||||||
|
t.Errorf("%s: expected %v, got %v", name, e, a)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// this varies every test
|
||||||
|
delete(target.headers, "X-Forwarded-Host")
|
||||||
|
if e, a := tc.expectedHeaders, target.headers; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("%s: expected %v, got %v", name, e, a)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,12 +20,14 @@ go_library(
|
|||||||
"//cmd/kubernetes-discovery/pkg/legacy:go_default_library",
|
"//cmd/kubernetes-discovery/pkg/legacy:go_default_library",
|
||||||
"//pkg/api:go_default_library",
|
"//pkg/api:go_default_library",
|
||||||
"//pkg/genericapiserver:go_default_library",
|
"//pkg/genericapiserver:go_default_library",
|
||||||
|
"//pkg/genericapiserver/filters:go_default_library",
|
||||||
"//pkg/genericapiserver/options:go_default_library",
|
"//pkg/genericapiserver/options:go_default_library",
|
||||||
"//pkg/kubectl/cmd/util:go_default_library",
|
"//pkg/kubectl/cmd/util:go_default_library",
|
||||||
"//pkg/registry/generic:go_default_library",
|
"//pkg/registry/generic:go_default_library",
|
||||||
"//pkg/registry/generic/registry:go_default_library",
|
"//pkg/registry/generic/registry:go_default_library",
|
||||||
"//pkg/runtime/schema:go_default_library",
|
"//pkg/runtime/schema:go_default_library",
|
||||||
"//pkg/storage/storagebackend:go_default_library",
|
"//pkg/storage/storagebackend:go_default_library",
|
||||||
|
"//pkg/util/sets:go_default_library",
|
||||||
"//pkg/util/wait:go_default_library",
|
"//pkg/util/wait:go_default_library",
|
||||||
"//vendor:github.com/pborman/uuid",
|
"//vendor:github.com/pborman/uuid",
|
||||||
"//vendor:github.com/spf13/cobra",
|
"//vendor:github.com/spf13/cobra",
|
||||||
|
@ -19,6 +19,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -27,12 +28,14 @@ import (
|
|||||||
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/legacy"
|
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/legacy"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/genericapiserver"
|
"k8s.io/kubernetes/pkg/genericapiserver"
|
||||||
|
"k8s.io/kubernetes/pkg/genericapiserver/filters"
|
||||||
genericoptions "k8s.io/kubernetes/pkg/genericapiserver/options"
|
genericoptions "k8s.io/kubernetes/pkg/genericapiserver/options"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
"k8s.io/kubernetes/pkg/registry/generic"
|
"k8s.io/kubernetes/pkg/registry/generic"
|
||||||
"k8s.io/kubernetes/pkg/registry/generic/registry"
|
"k8s.io/kubernetes/pkg/registry/generic/registry"
|
||||||
"k8s.io/kubernetes/pkg/runtime/schema"
|
"k8s.io/kubernetes/pkg/runtime/schema"
|
||||||
"k8s.io/kubernetes/pkg/storage/storagebackend"
|
"k8s.io/kubernetes/pkg/storage/storagebackend"
|
||||||
|
"k8s.io/kubernetes/pkg/util/sets"
|
||||||
"k8s.io/kubernetes/pkg/util/wait"
|
"k8s.io/kubernetes/pkg/util/wait"
|
||||||
|
|
||||||
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1"
|
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1"
|
||||||
@ -46,6 +49,11 @@ type DiscoveryServerOptions struct {
|
|||||||
Authentication *genericoptions.DelegatingAuthenticationOptions
|
Authentication *genericoptions.DelegatingAuthenticationOptions
|
||||||
Authorization *genericoptions.DelegatingAuthorizationOptions
|
Authorization *genericoptions.DelegatingAuthorizationOptions
|
||||||
|
|
||||||
|
// ProxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
|
||||||
|
// this to confirm the proxy's identity
|
||||||
|
ProxyClientCertFile string
|
||||||
|
ProxyClientKeyFile string
|
||||||
|
|
||||||
StdOut io.Writer
|
StdOut io.Writer
|
||||||
StdErr io.Writer
|
StdErr io.Writer
|
||||||
}
|
}
|
||||||
@ -81,6 +89,8 @@ func NewCommandStartDiscoveryServer(out, err io.Writer) *cobra.Command {
|
|||||||
o.SecureServing.AddFlags(flags)
|
o.SecureServing.AddFlags(flags)
|
||||||
o.Authentication.AddFlags(flags)
|
o.Authentication.AddFlags(flags)
|
||||||
o.Authorization.AddFlags(flags)
|
o.Authorization.AddFlags(flags)
|
||||||
|
flags.StringVar(&o.ProxyClientCertFile, "proxy-client-cert-file", o.ProxyClientCertFile, "client certificate used identify the proxy to the API server")
|
||||||
|
flags.StringVar(&o.ProxyClientKeyFile, "proxy-client-key-file", o.ProxyClientKeyFile, "client certificate key used identify the proxy to the API server")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@ -114,6 +124,10 @@ func (o DiscoveryServerOptions) RunDiscoveryServer() error {
|
|||||||
if _, err := genericAPIServerConfig.ApplyDelegatingAuthorizationOptions(o.Authorization); err != nil {
|
if _, err := genericAPIServerConfig.ApplyDelegatingAuthorizationOptions(o.Authorization); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
genericAPIServerConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
|
||||||
|
sets.NewString("watch", "proxy"),
|
||||||
|
sets.NewString("attach", "exec", "proxy", "log", "portforward"),
|
||||||
|
)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
privilegedLoopbackToken := uuid.NewRandom().String()
|
privilegedLoopbackToken := uuid.NewRandom().String()
|
||||||
@ -126,6 +140,15 @@ func (o DiscoveryServerOptions) RunDiscoveryServer() error {
|
|||||||
RESTOptionsGetter: &restOptionsFactory{storageConfig: &o.Etcd.StorageConfig},
|
RESTOptionsGetter: &restOptionsFactory{storageConfig: &o.Etcd.StorageConfig},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.ProxyClientCert, err = ioutil.ReadFile(o.ProxyClientCertFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.ProxyClientKey, err = ioutil.ReadFile(o.ProxyClientKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
server, err := config.Complete().New()
|
server, err := config.Complete().New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -469,6 +469,8 @@ prom-push-gateway
|
|||||||
protect-kernel-defaults
|
protect-kernel-defaults
|
||||||
proto-import
|
proto-import
|
||||||
proxy-bindall
|
proxy-bindall
|
||||||
|
proxy-client-cert-file
|
||||||
|
proxy-client-key-file
|
||||||
proxy-kubeconfig
|
proxy-kubeconfig
|
||||||
proxy-logv
|
proxy-logv
|
||||||
proxy-mode
|
proxy-mode
|
||||||
|
Loading…
Reference in New Issue
Block a user